pi-skill-evolve 0.1.5 → 0.1.7

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.
Files changed (28) hide show
  1. package/extensions/skill-evolve/lib/runs.ts +4 -1
  2. package/extensions/skill-evolve/lib/web/list-query.ts +43 -0
  3. package/extensions/skill-evolve/lib/web/markdown-lite.ts +95 -0
  4. package/extensions/skill-evolve/lib/web/pending.ts +5 -5
  5. package/extensions/skill-evolve/lib/web/render-controls.ts +58 -32
  6. package/extensions/skill-evolve/lib/web/render-rows.ts +178 -235
  7. package/extensions/skill-evolve/lib/web/render-session.ts +105 -34
  8. package/extensions/skill-evolve/lib/web/render-skills.ts +0 -1
  9. package/extensions/skill-evolve/lib/web/render-table.ts +126 -0
  10. package/extensions/skill-evolve/lib/web/render.ts +116 -54
  11. package/extensions/skill-evolve/lib/web/server.ts +9 -1
  12. package/extensions/skill-evolve/lib/web/session-render.ts +189 -0
  13. package/extensions/skill-evolve/lib/web/session-view.ts +100 -84
  14. package/extensions/skill-evolve/lib/web/shell.ts +42 -11
  15. package/extensions/skill-evolve/lib/web/style.ts +171 -106
  16. package/extensions/skill-evolve/lib/web/text.ts +57 -0
  17. package/extensions/skill-evolve/test/pending-kinds.test.ts +142 -0
  18. package/extensions/skill-evolve/test/session-nav.test.ts +162 -0
  19. package/extensions/skill-evolve/test/session-page.test.ts +11 -5
  20. package/extensions/skill-evolve/test/session-render.test.ts +203 -0
  21. package/extensions/skill-evolve/test/session-view.test.ts +75 -87
  22. package/extensions/skill-evolve/test/web-applied.test.ts +30 -22
  23. package/extensions/skill-evolve/test/web-redesign.test.ts +264 -0
  24. package/extensions/skill-evolve/test/web-render.test.ts +57 -41
  25. package/extensions/skill-evolve/test/web-server.test.ts +4 -1
  26. package/extensions/skill-evolve/test/web-session-column.test.ts +30 -32
  27. package/extensions/skill-evolve/test/web-shell.test.ts +7 -5
  28. package/package.json +1 -1
@@ -1,27 +1,24 @@
1
1
  /**
2
- * 列表页的「会话」列:五个视图(待处理 / 待审批 / 已采纳 / 已处理 / 复盘报告)都必须带,
3
- * 且文案一致 —— 只显示 sessionId 前 8 位,完整 id 放 title,点进 `?view=session`。
2
+ * 列表页的「会话」标识:五个视图(待处理 / 待审批 / 已采纳 / 已处理 / 复盘报告)都必须能看出这条 run 属于哪段对话,
3
+ * 且文案一致 —— 显示 sessionId 前 8 位,完整 id 放 title,点进 `?view=session`。
4
4
  *
5
5
  * 为什么必须每页都有(用户 2026-11-08):「面板展示的也应该是 pi sessionId 否则我都不知道怎么看对应会话」。
6
- * 同一个渲染函数 `sessionCell` 供五处复用,本文件就是把这条约定钉住:
7
- * 少一页就说明有人另写了一份行渲染。
6
+ * 重做列表页后会话不再是一列:同一会话的多条 run 归到组标题(`<details>` 的 summary)里,
7
+ * 单条会话的 run 把会话名写在状态格的小字里 —— 两处都由 render-rows.ts 产出,本文件把这条约定钉住。
8
8
  */
9
9
  import assert from "node:assert/strict";
10
10
  import test from "node:test";
11
11
  import type { AppliedItem } from "../lib/web/applied.ts";
12
12
  import type { ApprovalItem } from "../lib/web/approvals.ts";
13
- import type { QueueItem } from "../lib/queue.ts";
14
13
  import type { DoneItem, PendingItem, WebState } from "../lib/web/data.ts";
15
14
  import type { RunReport } from "../lib/web/reports.ts";
16
15
  import { renderPage } from "../lib/web/render.ts";
17
- import { pendingRows } from "../lib/web/render-rows.ts";
18
16
  import type { SessionRef } from "../lib/web/run-session.ts";
19
17
 
20
18
  const NOW = new Date(2026, 10, 8, 12).getTime();
21
19
  const SESSION_ID = "01a1b90f-08c3-722c-871d-d024b083770a";
22
20
  const SESSION: SessionRef = { sessionId: SESSION_ID, sessionFile: `/tmp/${SESSION_ID}.jsonl` };
23
21
  const NO_SESSION: SessionRef = { sessionId: null, sessionFile: null };
24
- const CTX = { csrf: "c", writable: true, view: "pending" as const };
25
22
 
26
23
  function state(parts: Partial<WebState> = {}): WebState {
27
24
  return {
@@ -89,53 +86,54 @@ function reportItem(session: SessionRef): RunReport {
89
86
 
90
87
  /** 五个视图各塞一条带会话的行,返回整页 HTML */
91
88
  function pageWithSession(): Record<string, string> {
92
- const base: Partial<WebState> = {
89
+ const current = state({
93
90
  pending: [pendingItem(SESSION)],
94
91
  approvals: [approvalItem(SESSION)],
95
92
  applied: [appliedItem(SESSION)],
96
93
  done: [doneItem(SESSION)],
97
94
  reports: [reportItem(SESSION)],
98
- };
99
- const current = state(base);
95
+ });
100
96
  return {
101
- pending: renderPage(current, "pending"),
102
- approvals: renderPage(current, "approvals"),
103
- applied: renderPage(current, "applied"),
104
- done: renderPage(current, "done"),
105
- reports: renderPage(current, "reports"),
97
+ pending: renderPage(current, "pending", { now: NOW }),
98
+ approvals: renderPage(current, "approvals", { now: NOW }),
99
+ applied: renderPage(current, "applied", { now: NOW }),
100
+ done: renderPage(current, "done", { now: NOW }),
101
+ reports: renderPage(current, "reports", { now: NOW }),
106
102
  };
107
103
  }
108
104
 
109
- test("五个视图都有会话列:链接指向 ?view=session,文案是 sessionId 前 8 位,title 是完整 id", () => {
105
+ test("五个视图都能看到会话:链接指向 ?view=session,文案是 sessionId 前 8 位,title 是完整 id", () => {
110
106
  const pages = pageWithSession();
107
+ const href = `/?view=session&amp;sessionId=${SESSION_ID}`;
111
108
  for (const [view, html] of Object.entries(pages)) {
112
- const href = `/?view=session&amp;sessionId=${SESSION_ID}`;
113
109
  assert.equal(html.includes(href), true, `${view} 页缺会话链接`);
114
- assert.match(html, /会话 01a1b90f/, `${view} 页会话列要显示前 8 位`);
110
+ assert.match(html, /会话 01a1b90f/, `${view} 页要显示会话前 8 位`);
115
111
  assert.equal(html.includes(`title="${SESSION_ID}"`), true, `${view} 页 title 要放完整 sessionId`);
116
112
  }
117
113
  });
118
114
 
119
- test("待处理行:会话列跟在 runId 后面,同一行里两者都在", () => {
120
- const html = pendingRows([pendingItem(SESSION)], CTX);
121
- const runIdIndex = html.indexOf("run-p-1794133942");
122
- const sessIndex = html.indexOf('class="sess"');
123
- assert.equal(runIdIndex >= 0 && sessIndex > runIdIndex, true, "会话列应在 runId 之后");
124
- assert.match(html, / · <a class="sess"/, "会话列与行头之间用 · 分隔");
125
- });
126
-
127
- test("待审批行:summary 里同样带会话列(它是自己拼的 details,最容易漏)", () => {
128
- const html = renderPage(state({ approvals: [approvalItem(SESSION)] }), "approvals");
129
- assert.match(html, /<summary class="head">[^<]*<a class="sess" href="\/\?view=session&amp;sessionId=/);
115
+ test("单条会话的 run 不进组:会话名写在状态格的小字里,runId 只出现在操作列", () => {
116
+ const html = renderPage(state({ pending: [pendingItem(SESSION)] }), "pending", { now: NOW });
117
+ assert.match(html, /<span class="sk-sess"><a class="sess"/, "会话名应在状态格的小字里");
118
+ assert.equal(html.includes('<details class="grp"'), false, "只有一条 run 时不该出现会话分组");
119
+ // runId 不再是主标识:只在操作列,显示末 8 位,title 放完整值
120
+ assert.match(html, /<td class="ops">.*title="run-p-1794133942"/);
130
121
  });
131
122
 
132
123
  test("没有 sessionId 的老 run:写「会话 —」占位,不造空链接、不报错", () => {
133
- const html = renderPage(state({ pending: [pendingItem(NO_SESSION)], done: [doneItem(NO_SESSION)] }), "pending");
134
- assert.match(html, /<span class="hint">会话 —<\/span>/);
124
+ const html = renderPage(state({ pending: [pendingItem(NO_SESSION)], done: [doneItem(NO_SESSION)] }), "pending", {
125
+ now: NOW,
126
+ });
127
+ assert.match(html, /<span class="sk-sess">会话 —<\/span>/);
135
128
  assert.equal(html.includes("view=session"), false, "缺 id 时不许出现会话链接");
136
129
  });
137
130
 
138
131
  test("会话链接带 file 参数:页面可直接打开声明过的会话文件,不必再反查", () => {
139
- const html = pendingRows([pendingItem(SESSION)], CTX);
132
+ const html = renderPage(state({ pending: [pendingItem(SESSION)] }), "pending", { now: NOW });
140
133
  assert.match(html, new RegExp(`file=%2Ftmp%2F${SESSION_ID}\\.jsonl`));
141
134
  });
135
+
136
+ test("同一会话的多条 run 归到组标题:会话名出现在 details 的 summary 里", () => {
137
+ const html = renderPage(state({ pending: [pendingItem(SESSION), pendingItem(SESSION)] }), "pending", { now: NOW });
138
+ assert.match(html, /<details class="grp" open>\n<summary><a class="sess" href="\/\?view=session&amp;sessionId=/);
139
+ });
@@ -37,17 +37,19 @@ test("高亮只落在当前 tab 上;详情页(active=null)一个都不高
37
37
  assert.equal(tabs(null).includes('class="active"'), false);
38
38
  });
39
39
 
40
- test("状态栏:「已采纳 N」自己就是一个链接,点数字落到已采纳页", () => {
40
+ test("状态栏:四个数字,待批 / 已采纳各自是链接(回滚率不进这一行,它在周报里)", () => {
41
41
  const html = statusBar(state());
42
- assert.match(html, /复盘中 2 · 卡死 1 · 待批 <a href="\/\?view=approvals">3<\/a> · 已采纳 <a href="\/\?view=applied">4<\/a> · 回滚率 26%/);
42
+ assert.match(html, /复盘中 2 · 卡死 1 · 待批 <a href="\/\?view=approvals">3<\/a> · 已采纳 <a href="\/\?view=applied">4<\/a>/);
43
+ assert.equal(html.includes("回滚率"), false, "回滚率是周报口径,不该混进「现在要处理什么」这一行");
43
44
  });
44
45
 
45
46
  test("骨架:标题、状态栏、导航、正文、页脚各就各位", () => {
46
47
  const html = shellPage({ state: state(), title: "已采纳", active: "applied", body: "<main>正文</main>" });
47
48
  assert.match(html, /<title>技能进化 · 已采纳<\/title>/);
48
- assert.match(html, /<p class="bar">复盘中 2/);
49
- assert.match(html, /<nav>\n<a href="\/\?view=pending">待处理<\/a>/);
50
- assert.match(html, /<\/nav>\n<main>正文<\/main>\n<footer>/);
49
+ assert.match(html, /<p class="sum">复盘中 2/);
50
+ // tab 上带计数徽章(数量就是列表里的条数,不另算一份)
51
+ assert.match(html, /<nav class="tabs">\n<a href="\/\?view=pending">待处理<span class="cnt">0<\/span><\/a>/);
52
+ assert.equal(html.includes("</nav>\n</header>\n<main>正文</main>\n<footer>"), true, "nav 与正文之间只有粘性区结束标签");
51
53
  });
52
54
 
53
55
  test("?view= 白名单:三个新视图认,乱值一律回落待处理", () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-skill-evolve",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "技能进化:采集会话中的技能曝光/加载,异步复盘、生成改进提案并独立评审,附本机 Web 管理面板",
5
5
  "keywords": [
6
6
  "pi-package",