ework-web 0.10.74 → 0.10.76

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ework-web",
3
- "version": "0.10.74",
3
+ "version": "0.10.76",
4
4
  "type": "module",
5
5
  "description": "ework-web — standalone multi-project issue tracker. Local SQLite-backed, no external API dependency. Bun + TypeScript + SSR HTML.",
6
6
  "license": "MIT",
package/src/index.ts CHANGED
@@ -397,6 +397,7 @@ const REPO_LABEL_ADD_RE = /^\/([^/]+)\/([^/]+)\/settings\/labels\/add$/;
397
397
  const REPO_LABEL_ACTION_RE = /^\/([^/]+)\/([^/]+)\/settings\/labels\/(\d+)\/(update|archive|unarchive|delete)$/;
398
398
  const REPO_ISSUE_HALT_RE = /^\/([^/]+)\/([^/]+)\/issues\/(\d+)\/(halt|resume|dispatch-off|dispatch-on)$/;
399
399
  const REPO_ISSUE_MODEL_RE = /^\/([^/]+)\/([^/]+)\/issues\/(\d+)\/model$/;
400
+ const REPO_ISSUE_STATUS_RE = /^\/([^/]+)\/([^/]+)\/issues\/(\d+)\/ai-status$/;
400
401
  const API_ISSUE_LABELS_RE = /^\/api\/([^/]+)\/([^/]+)\/issues\/(\d+)\/labels$/;
401
402
  const WH_ACTION_RE = /^\/__wh\/(\d+)\/(delete|toggle|test)$/;
402
403
  const SESSIONS_RE = /^\/sessions$/;
@@ -1696,6 +1697,17 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
1696
1697
  }
1697
1698
  }
1698
1699
 
1700
+ const aiStatusMatch = url.pathname.match(REPO_ISSUE_STATUS_RE);
1701
+ if (aiStatusMatch && req.method === "GET") {
1702
+ const [, owner, repo, numStr] = aiStatusMatch;
1703
+ if (!(owner && repo && numStr)) return json({ error: "bad path" }, 400);
1704
+ const project = await getProject(owner, repo);
1705
+ if (!project) return json({ error: "project not found" }, 404);
1706
+ const issue = await getIssueWithMeta(project.id, Number(numStr));
1707
+ if (!issue) return json({ error: "issue not found" }, 404);
1708
+ return json({ aiStatus: issue.ai_status ?? "", commentCount: issue.comment_count ?? 0 });
1709
+ }
1710
+
1699
1711
  const issueLabelsApi = url.pathname.match(API_ISSUE_LABELS_RE);
1700
1712
  if (issueLabelsApi) {
1701
1713
  const [, owner, repo, numStr] = issueLabelsApi;
@@ -110,6 +110,7 @@ header.topbar .num{opacity:.7}
110
110
  .composer .btn-close.armed{background:#d23f31;color:#fff;border-color:#d23f31}
111
111
  .composer-ro{max-width:900px;margin:0 auto .9rem;padding:.7rem 1rem;color:var(--text-muted);font-size:13px;text-align:center;border:1px dashed var(--border);border-radius:8px}
112
112
  .tabs{display:flex;gap:.4rem;padding:.6rem 1rem;border-bottom:1px solid var(--border);max-width:900px;margin:0 auto;align-items:center}
113
+ .tabs .brand-tab{font-weight:600;margin-right:.4rem}
113
114
  .tab{padding:.4rem 1rem;border-radius:6px;font-size:14px;color:var(--text-muted)}
114
115
  .tab.active{background:var(--bg-muted);color:var(--text);font-weight:600}
115
116
  .tab-spacer{flex:1}
@@ -148,6 +149,7 @@ header.topbar .num{opacity:.7}
148
149
  .ai-dispatch-off{background:#6f7781;color:#fff}
149
150
  .ai-completed{background:#1a7f37;color:#fff}
150
151
  .ai-failed{background:#cf222e;color:#fff}
152
+ .ai-idle{background:#eaeef2;color:#57606a}
151
153
  @keyframes ai-pulse{0%,100%{opacity:1}50%{opacity:.6}}
152
154
  .action-btn{font-size:12px;padding:.15rem .6rem;border-radius:6px;border:1px solid var(--border);background:var(--bg-elev);color:#cf222e;cursor:pointer;font-weight:600}
153
155
  .action-btn:hover{background:#cf222e;color:#fff}
@@ -177,31 +179,32 @@ export function renderLayout(props: LayoutProps, inner: string, initialItems: st
177
179
  : "";
178
180
  const aiBadgeHtml = (() => {
179
181
  const s = props.aiStatus ?? "";
180
- // halted/dispatch_off are represented by their action buttons — skip badge
181
- if (!s || s === "halted" || s === "dispatch_off") return "";
182
182
  const map: Record<string, { cls: string; label: string }> = {
183
- processing: { cls: "ai-processing", label: "⚙️ 处理中" },
184
- completed: { cls: "ai-completed", label: " 已完成" },
185
- failed: { cls: "ai-failed", label: " 失败" },
183
+ processing: { cls: "ai-processing", label: "🔄 AI 处理中" },
184
+ completed: { cls: "ai-completed", label: " AI 已完成" },
185
+ failed: { cls: "ai-failed", label: "⚠️ AI 失败" },
186
+ halted: { cls: "ai-halted", label: "⏹ 已停止" },
187
+ dispatch_off: { cls: "ai-dispatch-off", label: "🔕 不接单中" },
188
+ idle: { cls: "ai-idle", label: "💤 空闲" },
186
189
  ...props.extraStatusBadges,
187
190
  };
188
- const m = map[s];
189
- if (!m) return "";
190
- return `<span class="ai-badge ${m.cls}">${m.label}</span>`;
191
+ const m = map[s] ?? { cls: "ai-idle", label: "💤 空闲" };
192
+ return `<span class="ai-badge ${m.cls}" id="aiStatusBadge" data-status="${escapeAttr(s)}">${m.label}</span>`;
191
193
  })();
192
- const isTerminal = props.aiStatus === "completed";
193
- const showActions = props.writesEnabled !== false && !isTerminal;
194
+ const showActions = props.writesEnabled !== false;
194
195
  const haltBtnHtml = showActions
195
196
  ? (props.aiStatus === "halted" || props.aiStatus === "failed")
196
197
  ? `<button type="button" class="action-btn resume-btn" data-action-href="${escapeAttr(repoIssuesHref)}/${props.issueNumber}/resume" data-action-confirm="确认恢复 AI 处理?" title="恢复 AI 处理">▶ 恢复</button>`
197
- : `<button type="button" class="action-btn" data-action-href="${escapeAttr(repoIssuesHref)}/${props.issueNumber}/halt" data-action-confirm="确认停止 AI 处理?" title="停止 AI 处理">⏹ 停止</button>`
198
+ : props.aiStatus === "processing"
199
+ ? `<button type="button" class="action-btn" id="aiStopBtn" data-action-href="${escapeAttr(repoIssuesHref)}/${props.issueNumber}/halt" data-action-confirm="确认停止 AI 处理?" title="停止正在运行的 AI">⏹ 停止运行</button>`
200
+ : ""
198
201
  : "";
199
202
  const dispatchBtnHtml = showActions
200
203
  ? props.projectDispatchOff
201
204
  ? `<span class="action-btn dispatch-btn" style="opacity:.5;cursor:not-allowed" title="项目已关闭自动接单">🔕 项目不接单</span>`
202
205
  : props.aiStatus === "dispatch_off"
203
- ? `<button type="button" class="action-btn dispatch-btn" data-action-href="${escapeAttr(repoIssuesHref)}/${props.issueNumber}/dispatch-on" title="允许自动接单">🔔 接单</button>`
204
- : `<button type="button" class="action-btn dispatch-btn" data-action-href="${escapeAttr(repoIssuesHref)}/${props.issueNumber}/dispatch-off" data-action-confirm="设为不自动接单?" title="设为不自动接单">🔕 不接单</button>`
206
+ ? `<button type="button" class="action-btn dispatch-btn" data-action-href="${escapeAttr(repoIssuesHref)}/${props.issueNumber}/dispatch-on" title="允许自动接单">🔔 恢复接单</button>`
207
+ : `<button type="button" class="action-btn dispatch-btn" data-action-href="${escapeAttr(repoIssuesHref)}/${props.issueNumber}/dispatch-off" data-action-confirm="设为不自动接单?" title="关闭此 issue 的自动接单">🔕 暂停接单</button>`
205
208
  : "";
206
209
  const modelSelectHtml = props.modelSelect && props.modelSelect.options.length > 0 && showActions
207
210
  ? `<span class="model-select-wrap"><select class="model-select" id="issueModelSelect" title="此 issue 的模型(覆盖项目/全局默认)">
@@ -335,7 +338,7 @@ export function tabNavHTML(active: "projects" | "issues" | "sessions" | "me", us
335
338
  const userArea = user
336
339
  ? `<span class="user-area"><a class="tab${m}" href="/me" title="${escapeAttr(user.login)}">👤 ${escapeHtml(user.login)}</a><form method="post" action="/logout" class="logout-form"><button type="submit" class="logout-btn" title="退出登录">退出</button></form></span>`
337
340
  : "";
338
- return `<nav class="tabs"><a class="tab${p}" href="/projects">项目</a><a class="tab${i}" href="/issues">Issues</a><a class="tab${s}" href="/sessions">会话</a><span style="margin-left:auto" class="tab-spacer"></span><a class="tab" href="/settings">⚙️ 设置</a>${userArea}</nav>`;
341
+ return `<nav class="tabs"><a class="tab brand-tab" href="/" title="ework 主页">🏠 ework</a><a class="tab${p}" href="/projects">项目</a><a class="tab${i}" href="/issues">Issues</a><a class="tab${s}" href="/sessions">会话</a><span style="margin-left:auto" class="tab-spacer"></span><a class="tab" href="/settings">⚙️ 设置</a>${userArea}</nav>`;
339
342
  }
340
343
 
341
344
  export interface ActionBarOpts {
@@ -6,6 +6,55 @@
6
6
  // data-action-confirm="确认停止?" — if set, confirm() first
7
7
  // data-action-reload="true" — default true, reload on success
8
8
  (function () {
9
+ var BADGES = {
10
+ "processing": ["ai-processing", "🔄 AI 处理中"],
11
+ "completed": ["ai-completed", "✅ AI 已完成"],
12
+ "failed": ["ai-failed", "⚠️ AI 失败"],
13
+ "halted": ["ai-halted", "⏹ 已停止"],
14
+ "dispatch_off": ["ai-dispatch-off", "🔕 不接单中"]
15
+ };
16
+
17
+ function issuePath() {
18
+ var m = location.pathname.match(/^(\/[^/]+\/[^/]+\/issues\/\d+)/);
19
+ return m ? m[1] : null;
20
+ }
21
+
22
+ function refreshBadge() {
23
+ var p = issuePath();
24
+ var badge = document.getElementById("aiStatusBadge");
25
+ if (!p || !badge) return;
26
+ fetch(p + "/ai-status", { headers: { Accept: "application/json" } })
27
+ .then(function (r) { return r.ok ? r.json() : null; })
28
+ .then(function (d) {
29
+ if (!d) return;
30
+ var b = BADGES[d.aiStatus] || ["ai-idle", "💤 空闲"];
31
+ badge.textContent = b[1];
32
+ badge.className = "ai-badge " + b[0];
33
+ var stopBtn = document.getElementById("aiStopBtn");
34
+ if (stopBtn) stopBtn.style.display = d.aiStatus === "processing" ? "" : "none";
35
+ var initial = Number(badge.getAttribute("data-comment-count") || 0);
36
+ if (d.commentCount > initial) {
37
+ var bar = document.getElementById("newReplyBar");
38
+ if (!bar) {
39
+ bar = document.createElement("div");
40
+ bar.id = "newReplyBar";
41
+ bar.style.cssText = "margin:.6rem 0;padding:.5rem .8rem;border-radius:8px;background:#ddf4ff;border:1px solid #54aeff;color:#0969da;cursor:pointer";
42
+ bar.textContent = "💬 有新回复 · 点击刷新查看";
43
+ bar.onclick = function () { location.reload(); };
44
+ badge.parentNode.insertBefore(bar, badge.nextSibling);
45
+ }
46
+ }
47
+ })
48
+ .catch(function () {});
49
+ }
50
+
51
+ if (document.getElementById("aiStatusBadge")) {
52
+ var bc = document.getElementById("aiStatusBadge");
53
+ var items = document.querySelectorAll('[id^="comment-"]');
54
+ bc.setAttribute("data-comment-count", String(items.length));
55
+ setInterval(refreshBadge, 8000);
56
+ }
57
+
9
58
  document.addEventListener("click", function (e) {
10
59
  var btn = e.target.closest("[data-action-href]");
11
60
  if (!btn) return;