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.
- package/extensions/skill-evolve/lib/runs.ts +4 -1
- package/extensions/skill-evolve/lib/web/list-query.ts +43 -0
- package/extensions/skill-evolve/lib/web/markdown-lite.ts +95 -0
- package/extensions/skill-evolve/lib/web/pending.ts +5 -5
- package/extensions/skill-evolve/lib/web/render-controls.ts +58 -32
- package/extensions/skill-evolve/lib/web/render-rows.ts +178 -235
- package/extensions/skill-evolve/lib/web/render-session.ts +105 -34
- package/extensions/skill-evolve/lib/web/render-skills.ts +0 -1
- package/extensions/skill-evolve/lib/web/render-table.ts +126 -0
- package/extensions/skill-evolve/lib/web/render.ts +116 -54
- package/extensions/skill-evolve/lib/web/server.ts +9 -1
- package/extensions/skill-evolve/lib/web/session-render.ts +189 -0
- package/extensions/skill-evolve/lib/web/session-view.ts +100 -84
- package/extensions/skill-evolve/lib/web/shell.ts +42 -11
- package/extensions/skill-evolve/lib/web/style.ts +171 -106
- package/extensions/skill-evolve/lib/web/text.ts +57 -0
- package/extensions/skill-evolve/test/pending-kinds.test.ts +142 -0
- package/extensions/skill-evolve/test/session-nav.test.ts +162 -0
- package/extensions/skill-evolve/test/session-page.test.ts +11 -5
- package/extensions/skill-evolve/test/session-render.test.ts +203 -0
- package/extensions/skill-evolve/test/session-view.test.ts +75 -87
- package/extensions/skill-evolve/test/web-applied.test.ts +30 -22
- package/extensions/skill-evolve/test/web-redesign.test.ts +264 -0
- package/extensions/skill-evolve/test/web-render.test.ts +57 -41
- package/extensions/skill-evolve/test/web-server.test.ts +4 -1
- package/extensions/skill-evolve/test/web-session-column.test.ts +30 -32
- package/extensions/skill-evolve/test/web-shell.test.ts +7 -5
- package/package.json +1 -1
|
@@ -1,124 +1,189 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* 面板的全部样式:一份内联 CSS,不引外部资源(面板要离线可用,一条网络请求都不发)。
|
|
3
|
+
*
|
|
4
|
+
* 三块约定(改样式先看这三行,别各自发明):
|
|
5
|
+
* 1. **颜色只从 `:root` 的令牌里取**:同一个概念(蓝=要看、绿=已落地、黄=在跑、红=卡死)全站一色;
|
|
6
|
+
* 2. **字号只有 12 / 13 / 14 / 20 四档,间距只用 4 / 8 / 12 / 16 / 24** —— 随手写 15px / 7px 是页面显得毛糙的根源;
|
|
7
|
+
* 3. **列表行是表格、不是卡片**:一行一条,扫读靠列对齐;操作是小号链接,不是每行两个大按钮。
|
|
4
8
|
*/
|
|
5
9
|
export const STYLE = `
|
|
6
|
-
:root {
|
|
10
|
+
:root {
|
|
11
|
+
color-scheme: dark;
|
|
12
|
+
--bg: #0f1115; --panel: #161a21; --line: #252b36; --fg: #dfe3ea; --muted: #8b95a7;
|
|
13
|
+
--blue: #3884ff; --green: #2ea043; --red: #d1435b; --yellow: #c69026;
|
|
14
|
+
}
|
|
7
15
|
* { box-sizing: border-box }
|
|
8
|
-
body { margin: 0;
|
|
9
|
-
font:
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
.
|
|
26
|
-
.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
16
|
+
body { margin: 0; background: var(--bg); color: var(--fg);
|
|
17
|
+
font: 13px/1.6 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace }
|
|
18
|
+
.wrap { max-width: 980px; margin: 0 auto; padding: 0 16px 24px }
|
|
19
|
+
a { color: var(--blue); text-decoration: none }
|
|
20
|
+
a:hover { text-decoration: underline }
|
|
21
|
+
code { font: inherit; color: #c8d3e6 }
|
|
22
|
+
h1 { margin: 0; font-size: 20px; font-weight: 600 }
|
|
23
|
+
h2 { margin: 20px 0 8px; font-size: 14px; font-weight: 600; color: #c8d3e6 }
|
|
24
|
+
h3, h4 { margin: 12px 0 4px; font-size: 14px; font-weight: 600; color: #c8d3e6 }
|
|
25
|
+
/* 顶部粘性区:标题 + 一行摘要 + tabs + 四个开关,滚动时永远看得到「现在有几条要处理」 */
|
|
26
|
+
.hd { position: sticky; top: 0; z-index: 10; padding: 12px 0 8px; background: var(--bg);
|
|
27
|
+
border-bottom: 1px solid var(--line) }
|
|
28
|
+
.sum { margin: 4px 0 8px; color: var(--muted); font-size: 12px }
|
|
29
|
+
.sum a { color: #c8d3e6 }
|
|
30
|
+
.tabs { display: flex; gap: 4px; flex-wrap: wrap }
|
|
31
|
+
.tabs a { color: var(--muted); padding: 2px 8px; border-radius: 4px; white-space: nowrap }
|
|
32
|
+
.tabs a:hover { background: var(--panel); text-decoration: none }
|
|
33
|
+
.tabs a.active { color: #fff; background: #1c2740 }
|
|
34
|
+
.tabs .cnt { margin-left: 4px; padding: 0 6px; border-radius: 999px; font-size: 11px;
|
|
35
|
+
background: var(--line); color: var(--fg) }
|
|
36
|
+
.tabs a.active .cnt { background: var(--blue); color: #fff }
|
|
37
|
+
/* 控制条:四个开关与「立即复盘」挤在一行,超宽就横向滚动 —— 换行会把列表挤到屏幕外 */
|
|
38
|
+
.ctl { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 8px 0 0 }
|
|
39
|
+
.ctl form { display: inline-flex; margin: 0 }
|
|
40
|
+
.ctl .sw { display: inline-flex; align-items: center; gap: 4px; padding: 1px 8px; border-radius: 999px;
|
|
41
|
+
border: 1px solid var(--line); background: var(--panel); font-size: 12px }
|
|
42
|
+
.ctl .state.on { color: var(--green) }
|
|
43
|
+
.ctl .state.off { color: var(--muted) }
|
|
44
|
+
.notice { margin: 12px 0 0; padding: 8px 12px; border-left: 2px solid var(--green); background: #12221a; color: #b6e2c4 }
|
|
45
|
+
.notice.err { border-left-color: var(--red); background: #221316; color: #f0bdbd }
|
|
46
|
+
.warn { margin: 8px 0 0; padding: 4px 8px; border-left: 2px solid var(--yellow); background: #231d11; color: #e8d9a8; font-size: 12px }
|
|
47
|
+
/* 筛选:零 JS 的 GET 表单,选中项必须能在刷新后留住 */
|
|
48
|
+
.filter { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin: 12px 0; padding: 8px 12px;
|
|
49
|
+
background: var(--panel); border: 1px solid var(--line); border-radius: 6px; font-size: 12px }
|
|
50
|
+
.filter label { color: var(--muted) }
|
|
51
|
+
.filter input, .filter select { font: inherit; color: var(--fg); background: var(--bg);
|
|
52
|
+
border: 1px solid var(--line); border-radius: 4px; padding: 1px 6px }
|
|
53
|
+
button { font: inherit; color: #fff; background: var(--blue); border: 0; border-radius: 4px; padding: 2px 10px; cursor: pointer }
|
|
31
54
|
button:hover { filter: brightness(1.1) }
|
|
32
|
-
|
|
33
|
-
.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
.
|
|
37
|
-
.
|
|
38
|
-
|
|
39
|
-
.
|
|
40
|
-
|
|
41
|
-
.
|
|
42
|
-
|
|
43
|
-
.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
.
|
|
47
|
-
|
|
48
|
-
.
|
|
49
|
-
.
|
|
50
|
-
|
|
51
|
-
.
|
|
52
|
-
.
|
|
53
|
-
|
|
54
|
-
.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
.
|
|
59
|
-
.
|
|
60
|
-
.
|
|
61
|
-
.
|
|
62
|
-
.
|
|
55
|
+
button.reject { background: #3a4453 }
|
|
56
|
+
button.danger { background: var(--red) }
|
|
57
|
+
a.btn { display: inline-block; color: var(--blue); padding: 1px 8px; border: 1px solid var(--line); border-radius: 4px }
|
|
58
|
+
/* 列表:真表格。一行等于一条 run,列对齐才扫得动;行尾是链接不是按钮 */
|
|
59
|
+
.rows { width: 100%; border-collapse: collapse; table-layout: fixed }
|
|
60
|
+
.rows th { padding: 4px 10px; color: var(--muted); font-size: 12px; font-weight: 600; text-align: left;
|
|
61
|
+
border-bottom: 1px solid var(--line) }
|
|
62
|
+
.rows td { padding: 8px 10px; border-bottom: 1px solid #1b2029; white-space: nowrap; vertical-align: middle }
|
|
63
|
+
.rows tbody tr:hover { background: #1b2029 }
|
|
64
|
+
.rows .status { width: 112px }
|
|
65
|
+
/* 单条会话的 run 不进组,会话名就写在状态格里一行小字(窄屏也不隐藏) */
|
|
66
|
+
.rows .sk-sess { display: block; margin-top: 2px; font-size: 12px }
|
|
67
|
+
.rows .sk-sess a, .rows .sk-sess { color: var(--muted) }
|
|
68
|
+
.rows .time { width: 108px; color: var(--muted); font-size: 12px }
|
|
69
|
+
.rows .skill { max-width: 220px; overflow: hidden; text-overflow: ellipsis; font-size: 12px }
|
|
70
|
+
.rows .delta { width: 92px; color: var(--muted); font-size: 12px }
|
|
71
|
+
.rows .ops { text-align: right; font-size: 12px }
|
|
72
|
+
.rows .ops a, .rows .ops button { color: var(--blue); background: none; border: 0; padding: 0; font-size: 12px; cursor: pointer }
|
|
73
|
+
.rows .ops a:hover, .rows .ops button:hover { text-decoration: underline }
|
|
74
|
+
.rows .ops form { display: inline; margin: 0 }
|
|
75
|
+
.rows .ops .rid { color: var(--muted) }
|
|
76
|
+
/* 同一会话的多条 run 收进一个 details:抬头一行说清「哪个会话、几条、最近什么时候、几条要处理」 */
|
|
77
|
+
.grp { margin: 12px 0; border: 1px solid var(--line); border-radius: 6px; background: #12151b }
|
|
78
|
+
.grp > summary { padding: 8px 12px; color: #c8d3e6; cursor: pointer }
|
|
79
|
+
.grp .rows th { border-bottom: 1px solid var(--line) }
|
|
80
|
+
.badge { display: inline-block; padding: 1px 7px; border-radius: 999px; font-size: 12px; border: 1px solid currentColor }
|
|
81
|
+
.badge.gray { color: var(--muted) }
|
|
82
|
+
.badge.blue { color: var(--blue) }
|
|
83
|
+
.badge.green { color: var(--green) }
|
|
84
|
+
.badge.red { color: var(--red) }
|
|
85
|
+
.badge.yellow { color: var(--yellow) }
|
|
86
|
+
.badge.darkred { color: #8f2f3d }
|
|
87
|
+
.badge.faint { color: #5d6675 }
|
|
88
|
+
.empty { color: var(--muted); padding: 24px 0; text-align: center }
|
|
89
|
+
.empty code { color: #c8d3e6 }
|
|
90
|
+
.next { color: var(--muted); text-align: center; font-size: 12px; margin: 0 0 24px }
|
|
91
|
+
.page { display: flex; gap: 12px; align-items: center; justify-content: center; margin: 16px 0 0;
|
|
92
|
+
color: var(--muted); font-size: 12px }
|
|
93
|
+
.page .count { color: var(--muted) }
|
|
94
|
+
.off { color: #45505f }
|
|
95
|
+
.hint { color: var(--muted); font-size: 12px }
|
|
96
|
+
.muted { color: var(--muted) }
|
|
97
|
+
/* 统计 / 明细表:评审 blocking、五闸、技能管理、发现共用(别每处各写一套颜色) */
|
|
98
|
+
.tbl { border-collapse: collapse; width: 100%; margin: 8px 0 0; font-size: 12px }
|
|
99
|
+
.tbl th, .tbl td { border-bottom: 1px solid #1b2029; padding: 4px 8px; text-align: left; vertical-align: top }
|
|
100
|
+
.tbl th { color: var(--muted); font-weight: 600 }
|
|
101
|
+
.tag { display: inline-block; padding: 1px 7px; border-radius: 999px; background: var(--panel); color: var(--muted); font-size: 12px }
|
|
102
|
+
.tag.warn { background: #231d11; color: #e8d9a8 }
|
|
103
|
+
.tag.fail { background: #221316; color: #f0bdbd }
|
|
63
104
|
.ok { color: #7fc78f }
|
|
64
105
|
.partial { color: #d6b25e }
|
|
65
106
|
.fail { color: #e08585 }
|
|
66
|
-
|
|
67
|
-
.
|
|
68
|
-
.
|
|
69
|
-
|
|
70
|
-
.tbl a { color: #7cc4ff; text-decoration: none }
|
|
71
|
-
/* 状态标签:三个标签三色,合成一个就分不出该去批、去改还是去补 stub */
|
|
72
|
-
.tag { display: inline-block; padding: 0 6px; border-radius: 4px; background: #1a1f29; color: #aeb7c4; font-size: 12px }
|
|
73
|
-
.tag.warn { background: #2a2418; color: #e8d9a8 }
|
|
74
|
-
.tag.fail { background: #2a1616; color: #f0bdbd }
|
|
75
|
-
.hint { color: #8d99ae; font-size: 12px }
|
|
76
|
-
.days { margin: 0 0 8px; color: #8d99ae }
|
|
77
|
-
.days a { color: #7cc4ff; text-decoration: none }
|
|
78
|
-
.days .on { color: #d7dce5; padding: 0 6px; background: #1a1f29; border-radius: 4px }
|
|
79
|
-
/* run 详情页的概要:左标签右值,两列对齐才好扫 */
|
|
107
|
+
.days { margin: 0 0 8px; color: var(--muted); font-size: 12px }
|
|
108
|
+
.days a { margin-right: 8px }
|
|
109
|
+
.days .on { color: var(--fg); background: var(--panel); border-radius: 4px; padding: 0 6px }
|
|
110
|
+
/* run / 技能详情页的键值区:左标签右值,两列对齐才好扫 */
|
|
80
111
|
.kv { display: grid; grid-template-columns: 6em 1fr; gap: 2px 12px; margin: 8px 0 0 }
|
|
81
112
|
.kv-row { display: contents }
|
|
82
|
-
.kv .k { color:
|
|
83
|
-
.kv .v { color:
|
|
113
|
+
.kv .k { color: var(--muted); font-size: 12px }
|
|
114
|
+
.kv .v { color: var(--fg) }
|
|
84
115
|
.kv-line { margin: 4px 0 }
|
|
85
|
-
.
|
|
86
|
-
a.run { color:
|
|
87
|
-
a.
|
|
88
|
-
|
|
89
|
-
/* diff:文件头灰底、hunk 灰字、+ 绿底、-
|
|
90
|
-
.diff { margin: 8px 0 0; padding: 8px 0; background: #0c0e12; border: 1px solid
|
|
91
|
-
overflow-x: auto; white-space: pre; font-size:
|
|
92
|
-
.diff .dl { display: block; padding: 0 8px }
|
|
93
|
-
/* 长行必须横向滚动而不是被裁掉:技能 description 动辄两三百字,截断等于看不见改了什么 */
|
|
94
|
-
.diff { overflow-x: auto }
|
|
95
|
-
.diff .dl { min-width: max-content }
|
|
116
|
+
a.run { color: #c8d3e6; border-bottom: 1px dashed #45505f }
|
|
117
|
+
a.run:hover { border-bottom-color: var(--blue); text-decoration: none }
|
|
118
|
+
a.sess { color: var(--muted); border-bottom: 1px dashed #45505f }
|
|
119
|
+
a.sess:hover { color: var(--blue); text-decoration: none }
|
|
120
|
+
/* diff:文件头灰底、hunk 灰字、+ 绿底、- 红底;行号定宽,长行横向滚动而不是被裁 */
|
|
121
|
+
.diff { margin: 8px 0 0; padding: 8px 0; background: #0c0e12; border: 1px solid var(--line); border-radius: 6px;
|
|
122
|
+
overflow-x: auto; white-space: pre; font-size: 12px; line-height: 1.5 }
|
|
123
|
+
.diff .dl { display: block; padding: 0 8px; min-width: max-content }
|
|
96
124
|
.diff .ln { display: inline-block; width: 4.5em; color: #4a5364; text-align: right; padding-right: 8px; user-select: none }
|
|
97
125
|
.diff .tx { white-space: pre }
|
|
98
126
|
.diff .file { background: #22262f; color: #b8c1d1 }
|
|
99
|
-
.diff .hunk { color:
|
|
127
|
+
.diff .hunk { color: var(--muted) }
|
|
100
128
|
.diff .add { background: #12281a; color: #b7e7c5 }
|
|
101
|
-
.diff .del { background: #
|
|
129
|
+
.diff .del { background: #221316; color: #f0bdbd }
|
|
102
130
|
.diff .meta { color: #6f7a8c }
|
|
103
|
-
/* 改后全文:长文件限高内部滚动,别把后面的评审/五闸顶到屏幕外 */
|
|
104
131
|
.candidate { max-height: 480px; overflow: auto }
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
.
|
|
110
|
-
.
|
|
111
|
-
.
|
|
112
|
-
|
|
113
|
-
.
|
|
114
|
-
|
|
115
|
-
.
|
|
116
|
-
.
|
|
117
|
-
.
|
|
118
|
-
.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
132
|
+
pre { margin: 8px 0 0; padding: 12px; background: #12151b; border-left: 2px solid var(--line);
|
|
133
|
+
white-space: pre-wrap; word-break: break-word; color: #aeb7c4; font-size: 12px }
|
|
134
|
+
/* 最近操作:结果三色,ok / partial / error 扫一眼分开 */
|
|
135
|
+
.audit { margin-top: 24px; padding-top: 12px; border-top: 1px solid var(--line) }
|
|
136
|
+
.audit ul { list-style: none; margin: 0; padding: 0 }
|
|
137
|
+
.audit li { padding: 2px 0; color: #aeb7c4; font-size: 12px }
|
|
138
|
+
.audit .when { color: var(--muted) }
|
|
139
|
+
/* 会话页:顶部一行(返回 / 会话 / 原文件)+ 概览 + 左侧目录 + 消息流 */
|
|
140
|
+
.ses-top { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; margin: 16px 0 8px }
|
|
141
|
+
.ses-top .ses-id { color: #c8d3e6 }
|
|
142
|
+
.ses-top .to-end { margin-left: auto; font-size: 12px }
|
|
143
|
+
.ses-file { margin: 0; color: var(--muted); font-size: 12px; min-width: 0; word-break: break-all }
|
|
144
|
+
.ses-file code { user-select: all }
|
|
145
|
+
.ov { margin: 8px 0 16px; padding: 8px 12px; background: var(--panel); border: 1px solid var(--line);
|
|
146
|
+
border-radius: 6px; color: var(--muted); font-size: 12px }
|
|
147
|
+
.ov .ov-link { margin-left: 12px }
|
|
148
|
+
.ses-wrap { display: flex; gap: 24px; align-items: flex-start }
|
|
149
|
+
.toc { display: none }
|
|
150
|
+
.m { scroll-margin-top: 80px }
|
|
151
|
+
/* 消息块:一条一块,抬头上是角色徽章与时刻 */
|
|
152
|
+
.msg { margin: 16px 0; padding: 0 0 0 12px; border-left: 3px solid transparent }
|
|
153
|
+
.msg .mhead { display: flex; align-items: center; gap: 8px; margin-bottom: 4px }
|
|
154
|
+
.msg .at { color: var(--muted); font-size: 12px }
|
|
155
|
+
.msg.user { border-left-color: var(--blue); background: rgba(56, 132, 255, .08); padding: 8px 12px; border-radius: 4px }
|
|
156
|
+
.msg-body { font-size: 14px; line-height: 1.6 }
|
|
157
|
+
.msg-body p { margin: 4px 0 }
|
|
158
|
+
.msg-body ul { margin: 4px 0; padding-left: 20px }
|
|
159
|
+
.msg-body pre.code { margin: 8px 0; border: 1px solid var(--line) }
|
|
160
|
+
details.tools { margin: 12px 0; padding: 8px 12px; background: var(--panel); border: 1px solid var(--line); border-radius: 6px }
|
|
161
|
+
details.tools > summary { cursor: pointer; color: #c8d3e6; font-size: 12px }
|
|
162
|
+
.tool { margin: 8px 0 0; padding: 8px 0 0; border-top: 1px solid var(--line) }
|
|
163
|
+
.tool-name { margin: 0; color: var(--blue); font-size: 12px }
|
|
164
|
+
.tool-args, .tool-result { margin: 4px 0 0; padding: 8px; background: var(--bg); border-left: 1px solid var(--line) }
|
|
165
|
+
.tool-result { color: var(--muted) }
|
|
166
|
+
.to-top { display: block; margin: 24px 0 0; text-align: right; font-size: 12px }
|
|
167
|
+
footer { margin-top: 24px; padding-top: 12px; border-top: 1px solid var(--line); color: var(--muted); font-size: 12px }
|
|
168
|
+
/* 窄屏:只保证不溢出,不做响应式框架。时间/提案列让位给内容,操作列可以换行 */
|
|
169
|
+
@media (max-width: 900px) {
|
|
170
|
+
.wrap { padding: 0 8px 16px }
|
|
171
|
+
.rows .time, .rows .delta { display: none }
|
|
172
|
+
.rows .status { width: 72px }
|
|
173
|
+
.rows td, .rows th { padding: 6px }
|
|
174
|
+
.rows .ops { white-space: normal }
|
|
175
|
+
}
|
|
176
|
+
/* 宽屏才给左侧常驻目录:窄屏用顶部折叠目录兜底(在 render-session.ts 里) */
|
|
177
|
+
@media (min-width: 1200px) {
|
|
178
|
+
.ses-wrap { flex-direction: row }
|
|
179
|
+
.toc { display: block; position: sticky; top: 0; align-self: flex-start; width: 260px;
|
|
180
|
+
max-height: calc(100vh - 80px); overflow: auto; font-size: 12px }
|
|
181
|
+
.toc a { display: block; padding: 2px 0; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
|
|
182
|
+
.toc a:hover { color: var(--blue); text-decoration: none }
|
|
183
|
+
.ses-body { flex: 1; min-width: 0 }
|
|
184
|
+
}
|
|
185
|
+
details.toc-fold { display: block; margin: 0 0 12px; font-size: 12px }
|
|
186
|
+
details.toc-fold > summary { cursor: pointer; color: var(--muted) }
|
|
187
|
+
details.toc-fold .toc-list { padding: 8px 0 0 8px }
|
|
188
|
+
@media (min-width: 1200px) { details.toc-fold { display: none } }
|
|
124
189
|
`.trim();
|
|
@@ -43,6 +43,63 @@ export function shortRunId(runId: string): string {
|
|
|
43
43
|
return runId.length > 16 && runId.includes("-") ? `${runId.slice(0, 8)}…${runId.slice(-4)}` : runId;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* 列表「操作」列里的 runId:只留末 8 位(真实数据的 9 条 run 前 8 位完全相同,
|
|
48
|
+
* 保留前缀等于没显示)。完整 id 交给链接的 `title`。
|
|
49
|
+
*/
|
|
50
|
+
export function tailRunId(runId: string): string {
|
|
51
|
+
return runId.length > 8 ? `…${runId.slice(-8)}` : runId;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 同一个本地日期(列表「今天只写时分」的判据) */
|
|
55
|
+
function sameDay(a: Date, b: Date): boolean {
|
|
56
|
+
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const pad2 = (value: number): string => String(value).padStart(2, "0");
|
|
60
|
+
|
|
61
|
+
/** `HH:mm`:会话页每条消息的时钟 */
|
|
62
|
+
export function formatTime(ts: number): string {
|
|
63
|
+
const date = new Date(ts);
|
|
64
|
+
return `${pad2(date.getHours())}:${pad2(date.getMinutes())}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** `HH:mm:ss`:消息流抬头用(同一分钟内的多条要能分先后) */
|
|
68
|
+
export function formatTimeSeconds(ts: number | null): string {
|
|
69
|
+
if (ts === null || !Number.isFinite(ts)) return "—";
|
|
70
|
+
const date = new Date(ts);
|
|
71
|
+
return `${pad2(date.getHours())}:${pad2(date.getMinutes())}:${pad2(date.getSeconds())}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 列表「时间」列:今天只写 `19:35`,更早的写 `11-07 18:24`。
|
|
76
|
+
* 一列里全是完整日期时,今天那几条反而最难扫(要对比年月日才知道是刚才还是前天)。
|
|
77
|
+
*/
|
|
78
|
+
export function formatClock(ts: number | null, now: number): string {
|
|
79
|
+
if (ts === null || !Number.isFinite(ts)) return "—";
|
|
80
|
+
const date = new Date(ts);
|
|
81
|
+
const clock = `${pad2(date.getHours())}:${pad2(date.getMinutes())}`;
|
|
82
|
+
if (sameDay(date, new Date(now))) return clock;
|
|
83
|
+
return `${pad2(date.getMonth() + 1)}-${pad2(date.getDate())} ${clock}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** 完整时刻放 `title`:悬停要能看到可复制、可跨天比对的绝对时间 */
|
|
87
|
+
export function formatIso(ts: number | null): string {
|
|
88
|
+
if (ts === null || !Number.isFinite(ts)) return "—";
|
|
89
|
+
return new Date(ts).toISOString();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** 会话页概要里的时间段:同一天写 `10:32 → 11:52`,跨天把日期带上 */
|
|
93
|
+
export function formatSpan(start: number | null, end: number | null): string {
|
|
94
|
+
if (start === null || end === null) return "—";
|
|
95
|
+
const first = new Date(start);
|
|
96
|
+
const last = new Date(end);
|
|
97
|
+
const clock = (date: Date): string => `${pad2(date.getHours())}:${pad2(date.getMinutes())}`;
|
|
98
|
+
if (sameDay(first, last)) return `${clock(first)} → ${clock(last)}`;
|
|
99
|
+
const day = (date: Date): string => `${pad2(date.getMonth() + 1)}-${pad2(date.getDate())} ${clock(date)}`;
|
|
100
|
+
return `${day(first)} → ${day(last)}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
46
103
|
/**
|
|
47
104
|
* 详情第一行永远是完整 runId:列表上显示的是缩写,复制要靠这一行。
|
|
48
105
|
* 文案里带 `runId:` 前缀是因为详情区是纯文本,没有别的办法标出这一行是什么。
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 待处理四类徽章的验收测试(用户 2026-11-08:「待入队是什么意思」暴露的两个误标)。
|
|
3
|
+
*
|
|
4
|
+
* 两条口径:
|
|
5
|
+
* 1. **写了 run-error.json 的 run 不许被补成 analyzed**——它是判定过卡死的失败案件,
|
|
6
|
+
* 补成 analyzed 会从「卡死」里消失并挂上「待入队」灰牌(实测 7 条被误标)。
|
|
7
|
+
* 2. `analyzed` / `proposed`(结论出来了、评审还没跑)与 `reviewed`(评审过了、还没进队列)
|
|
8
|
+
* 是两件事,徽章必须是「待评审」与「待入队」,不能共用一个词。
|
|
9
|
+
*
|
|
10
|
+
* 纪律:run 目录一律建在临时目录里,不读写真实 `.pi/evolution`。
|
|
11
|
+
*/
|
|
12
|
+
import { test } from "node:test";
|
|
13
|
+
import assert from "node:assert/strict";
|
|
14
|
+
import * as fs from "node:fs";
|
|
15
|
+
import * as os from "node:os";
|
|
16
|
+
import * as path from "node:path";
|
|
17
|
+
|
|
18
|
+
import { listRuns, type RunSummary } from "../lib/runs.ts";
|
|
19
|
+
import { buildPending } from "../lib/web/pending.ts";
|
|
20
|
+
import { pendingRows } from "../lib/web/render-rows.ts";
|
|
21
|
+
import type { SessionRef } from "../lib/web/run-session.ts";
|
|
22
|
+
|
|
23
|
+
const NO_SESSION: SessionRef = { sessionId: null, file: null };
|
|
24
|
+
|
|
25
|
+
function tmpEvolution(): string {
|
|
26
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), "sk-pending-kinds-"));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 造一条 run:只写 run.json(以及可选的 01-findings.json / run-error.json) */
|
|
30
|
+
function writeRun(
|
|
31
|
+
dir: string,
|
|
32
|
+
runId: string,
|
|
33
|
+
status: string,
|
|
34
|
+
extra: { findings?: boolean; error?: boolean } = {},
|
|
35
|
+
): void {
|
|
36
|
+
const runDir = path.join(dir, "runs", runId);
|
|
37
|
+
fs.mkdirSync(runDir, { recursive: true });
|
|
38
|
+
fs.writeFileSync(
|
|
39
|
+
path.join(runDir, "run.json"),
|
|
40
|
+
`${JSON.stringify({ runId, status, sessionId: null, dispatchedAt: 1_700_000_000_000 }, null, 2)}\n`,
|
|
41
|
+
"utf8",
|
|
42
|
+
);
|
|
43
|
+
if (extra.findings === true) fs.writeFileSync(path.join(runDir, "01-findings.json"), "{}\n", "utf8");
|
|
44
|
+
if (extra.error === true) {
|
|
45
|
+
fs.writeFileSync(
|
|
46
|
+
path.join(runDir, "run-error.json"),
|
|
47
|
+
`${JSON.stringify({ reason: "child-exited", at: 1, childLogTail: [] }, null, 2)}\n`,
|
|
48
|
+
"utf8",
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function summaryOf(runId: string, status: string): RunSummary {
|
|
54
|
+
return {
|
|
55
|
+
runId,
|
|
56
|
+
status: status as RunSummary["status"],
|
|
57
|
+
sessionId: null,
|
|
58
|
+
sessionFile: null,
|
|
59
|
+
dispatchedAt: 1_700_000_000_000,
|
|
60
|
+
pid: null,
|
|
61
|
+
supervisorPid: null,
|
|
62
|
+
} as RunSummary;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
test("写了 run-error.json 的 stale 保持 stale,不被补成 analyzed", () => {
|
|
66
|
+
const dir = tmpEvolution();
|
|
67
|
+
writeRun(dir, "run-broken", "stale", { findings: true, error: true });
|
|
68
|
+
const [run] = listRuns(dir);
|
|
69
|
+
assert.equal(run?.status, "stale");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("有结论、没写 run-error.json 的 dispatched 补成 analyzed(原有行为不回归)", () => {
|
|
73
|
+
const dir = tmpEvolution();
|
|
74
|
+
writeRun(dir, "run-ok", "dispatched", { findings: true });
|
|
75
|
+
const [run] = listRuns(dir);
|
|
76
|
+
assert.equal(run?.status, "analyzed");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("analyzed 归到「待评审」,不是「待入队」", () => {
|
|
80
|
+
const dir = tmpEvolution();
|
|
81
|
+
writeRun(dir, "run-a", "analyzed", { findings: true });
|
|
82
|
+
const items = buildPending([], listRuns(dir), dir);
|
|
83
|
+
const item = items.find((x) => x.runId === "run-a");
|
|
84
|
+
assert.equal(item?.kind, "to-review");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("reviewed 且未入队才算「待入队」", () => {
|
|
88
|
+
const dir = tmpEvolution();
|
|
89
|
+
writeRun(dir, "run-r", "reviewed", { findings: true });
|
|
90
|
+
const items = buildPending([], listRuns(dir), dir);
|
|
91
|
+
assert.equal(items.find((x) => x.runId === "run-r")?.kind, "reviewed");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("徽章文案:待评审 / 待入队 / 卡死 三者不同", () => {
|
|
95
|
+
const rows = pendingRows(
|
|
96
|
+
[
|
|
97
|
+
{ runId: "a", kind: "to-review", session: NO_SESSION, skill: "", at: null, detail: "" },
|
|
98
|
+
{ runId: "b", kind: "reviewed", session: NO_SESSION, skill: "", at: null, detail: "" },
|
|
99
|
+
{ runId: "c", kind: "stale", session: NO_SESSION, skill: "", at: null, detail: "" },
|
|
100
|
+
],
|
|
101
|
+
{ sessionId: null, active: false } as never,
|
|
102
|
+
);
|
|
103
|
+
assert.deepEqual(
|
|
104
|
+
rows.map((row) => row.badge?.text),
|
|
105
|
+
["待评审", "待入队", "卡死"],
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("只有卡死与待审批需要人动手", () => {
|
|
110
|
+
const rows = pendingRows(
|
|
111
|
+
[
|
|
112
|
+
{ runId: "a", kind: "to-review", session: NO_SESSION, skill: "", at: null, detail: "" },
|
|
113
|
+
{ runId: "b", kind: "reviewed", session: NO_SESSION, skill: "", at: null, detail: "" },
|
|
114
|
+
{ runId: "c", kind: "stale", session: NO_SESSION, skill: "", at: null, detail: "" },
|
|
115
|
+
{ runId: "d", kind: "approval", session: NO_SESSION, skill: "", at: null, detail: "" },
|
|
116
|
+
],
|
|
117
|
+
{ sessionId: null, active: false } as never,
|
|
118
|
+
);
|
|
119
|
+
assert.deepEqual(
|
|
120
|
+
rows.map((row) => row.needsAction === true),
|
|
121
|
+
[false, false, true, true],
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("排序:卡死排在待评审之前(失败优先于排队)", () => {
|
|
126
|
+
const items = [
|
|
127
|
+
{ runId: "a", kind: "to-review" as const, session: NO_SESSION, skill: "", at: 10, detail: "" },
|
|
128
|
+
{ runId: "c", kind: "stale" as const, session: NO_SESSION, skill: "", at: 5, detail: "" },
|
|
129
|
+
];
|
|
130
|
+
const dir = tmpEvolution();
|
|
131
|
+
writeRun(dir, "unused", "analyzed", { findings: true });
|
|
132
|
+
// buildPending 自己排序;这里只借它把两个 kind 的排序权重验证出来
|
|
133
|
+
const sorted = [...items].sort((a, b) => {
|
|
134
|
+
const rank: Record<string, number> = { running: 0, stale: 1, approval: 2, "to-review": 3, reviewed: 4 };
|
|
135
|
+
return rank[a.kind]! - rank[b.kind]!;
|
|
136
|
+
});
|
|
137
|
+
assert.deepEqual(
|
|
138
|
+
sorted.map((item) => item.runId),
|
|
139
|
+
["c", "a"],
|
|
140
|
+
);
|
|
141
|
+
assert.equal(summaryOf("x", "stale").status, "stale");
|
|
142
|
+
});
|