peaks-loop 4.0.8 → 4.0.10
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/CHANGELOG.md +38 -0
- package/dist/cli/commands/_register.js +2 -0
- package/dist/cli/commands/baseline-commands.d.ts +3 -0
- package/dist/cli/commands/baseline-commands.js +146 -0
- package/dist/cli/commands/container-commands.js +2 -1
- package/dist/cli/commands/core/skill-command.js +32 -5
- package/dist/cli/commands/openspec-commands.js +2 -1
- package/dist/cli/commands/statusline-commands.js +1 -1
- package/dist/services/audit/enforcers/active-skill-resolver.d.ts +11 -0
- package/dist/services/audit/enforcers/active-skill-resolver.js +50 -39
- package/dist/services/capability-audit-service/cross-check.d.ts +8 -0
- package/dist/services/capability-audit-service/cross-check.js +11 -0
- package/dist/services/capability-audit-service/index.d.ts +3 -0
- package/dist/services/capability-audit-service/index.js +2 -0
- package/dist/services/capability-audit-service/runner.d.ts +27 -0
- package/dist/services/capability-audit-service/runner.js +40 -0
- package/dist/services/capability-audit-service/staleness.d.ts +1 -0
- package/dist/services/capability-audit-service/staleness.js +7 -0
- package/dist/services/capability-audit-service/types.d.ts +24 -0
- package/dist/services/capability-audit-service/types.js +1 -0
- package/dist/services/capability-baseline/store.d.ts +33 -0
- package/dist/services/capability-baseline/store.js +80 -0
- package/dist/services/capability-baseline/types.d.ts +45 -0
- package/dist/services/capability-baseline/types.js +5 -0
- package/dist/services/capability-baseline/validator.d.ts +19 -0
- package/dist/services/capability-baseline/validator.js +38 -0
- package/dist/services/capability-guard-runner/contracts/J01.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J01.js +48 -0
- package/dist/services/capability-guard-runner/contracts/J02.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J02.js +34 -0
- package/dist/services/capability-guard-runner/contracts/J03.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J03.js +24 -0
- package/dist/services/capability-guard-runner/contracts/J04.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J04.js +40 -0
- package/dist/services/capability-guard-runner/contracts/J05.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J05.js +21 -0
- package/dist/services/capability-guard-runner/contracts/J06.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J06.js +43 -0
- package/dist/services/capability-guard-runner/contracts/J07.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J07.js +53 -0
- package/dist/services/capability-guard-runner/contracts/J08.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J08.js +43 -0
- package/dist/services/capability-guard-runner/contracts/J09.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J09.js +44 -0
- package/dist/services/capability-guard-runner/contracts/J10.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J10.js +40 -0
- package/dist/services/capability-guard-runner/contracts/J11.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J11.js +39 -0
- package/dist/services/capability-guard-runner/contracts/J12.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J12.js +34 -0
- package/dist/services/capability-guard-runner/contracts/J13.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J13.js +45 -0
- package/dist/services/capability-guard-runner/contracts/J14.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J14.js +36 -0
- package/dist/services/capability-guard-runner/contracts/J15.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J15.js +40 -0
- package/dist/services/capability-guard-runner/diff.d.ts +2 -0
- package/dist/services/capability-guard-runner/diff.js +7 -0
- package/dist/services/capability-guard-runner/runner.d.ts +9 -0
- package/dist/services/capability-guard-runner/runner.js +22 -0
- package/dist/services/capability-guard-runner/types.d.ts +39 -0
- package/dist/services/capability-guard-runner/types.js +1 -0
- package/dist/services/container/container-lease.js +2 -1
- package/dist/services/evolution/evolution-types.d.ts +16 -16
- package/dist/services/final-review/final-review-service.d.ts +8 -0
- package/dist/services/final-review/final-review-service.js +14 -0
- package/dist/services/final-review/final-review-types.d.ts +1 -1
- package/dist/services/impact/impact-scan-service.js +4 -3
- package/dist/services/job/job-progress-store.d.ts +2 -2
- package/dist/services/job/job-types.d.ts +26 -26
- package/dist/services/migrate-skill-name/migrate.js +2 -1
- package/dist/services/observability/observability-service.d.ts +2 -2
- package/dist/services/openspec/artifact-boundary.js +3 -2
- package/dist/services/openspec/coverage-evidence-reader.js +9 -8
- package/dist/services/prd/handoff-auto-regen.js +2 -1
- package/dist/services/scan/type-sanity-service.js +2 -1
- package/dist/services/sediment/json-schema.d.ts +4 -4
- package/dist/services/session/session-binding-bridge.js +17 -19
- package/dist/services/session/session-manager.js +36 -12
- package/dist/services/session/session-resume-service.d.ts +4 -0
- package/dist/services/session/session-resume-service.js +4 -0
- package/dist/services/skills/skill-statusline-renderer.d.ts +46 -56
- package/dist/services/skills/skill-statusline-renderer.js +335 -126
- package/dist/services/skills/skill-statusline-service.d.ts +6 -0
- package/dist/services/skills/skill-statusline-service.js +105 -7
- package/dist/services/vm/vm-lease.js +2 -1
- package/dist/services/workflow/workflow-autonomous-resume-helpers.js +3 -2
- package/dist/services/workspace/workspace-service.js +2 -1
- package/dist/services/worktree/worktree-lease.js +2 -1
- package/dist/shared/path-safety.js +3 -5
- package/dist/shared/path-utils.d.ts +48 -0
- package/dist/shared/path-utils.js +65 -1
- package/package.json +5 -4
|
@@ -28,12 +28,39 @@ const BRAND_RGB = '38;2;90;101;216';
|
|
|
28
28
|
const BRAND_SGR_OPEN = `\x1b[1;${BRAND_RGB}m`;
|
|
29
29
|
const BRAND_SGR_FULL_OPEN = `\x1b[${BRAND_RGB}m`;
|
|
30
30
|
const BRAND_SGR_CLOSE = '\x1b[0m';
|
|
31
|
+
/**
|
|
32
|
+
* Highlight color used by the marquee scan band. `#E0E0E0` bright-grey
|
|
33
|
+
* + bold = "bleached" foreground that reads as a scanner sweeping
|
|
34
|
+
* over the colored text. Kept distinct from brand white (`#FFFFFF`)
|
|
35
|
+
* because pure white on `#5A65D8` brand purple reads as a strobe;
|
|
36
|
+
* the off-white tint gives the same "scanner" affordance without the
|
|
37
|
+
* flicker.
|
|
38
|
+
*/
|
|
39
|
+
const HIGHLIGHT_RGB = '38;2;224;224;224';
|
|
40
|
+
const HIGHLIGHT_SGR_OPEN = `\x1b[1;${HIGHLIGHT_RGB}m`;
|
|
31
41
|
function accent(text) {
|
|
32
42
|
return `${BRAND_SGR_OPEN}${text}${BRAND_SGR_CLOSE}`;
|
|
33
43
|
}
|
|
34
44
|
function accentGlyph(glyph) {
|
|
35
45
|
return `${BRAND_SGR_OPEN}${glyph}${BRAND_SGR_CLOSE}`;
|
|
36
46
|
}
|
|
47
|
+
// (kept for the brandText/active-dot helpers that inline the same SGR; the
|
|
48
|
+
// buildPalette factory owns per-token SGR injection now and does not call
|
|
49
|
+
// through these wrappers.)
|
|
50
|
+
/**
|
|
51
|
+
* Render the supplied text in the brand purple, honouring the
|
|
52
|
+
* `noColor` flag. Returns plain text when NO_COLOR is set or the
|
|
53
|
+
* tier is ASCII. Used by the active / stale / compact renders for
|
|
54
|
+
* tokens that the {@link buildPalette} cannot pre-stamp (skill
|
|
55
|
+
* name, mode, age, attention labels).
|
|
56
|
+
*/
|
|
57
|
+
function brandRun(text, noColor, capability) {
|
|
58
|
+
if (text.length === 0)
|
|
59
|
+
return text;
|
|
60
|
+
if (noColor || capability === 'ascii')
|
|
61
|
+
return text;
|
|
62
|
+
return `${BRAND_SGR_OPEN}${text}${BRAND_SGR_CLOSE}`;
|
|
63
|
+
}
|
|
37
64
|
/**
|
|
38
65
|
* Slow-blink variant of the accent glyph. Used for the idle indicator
|
|
39
66
|
* so the user perceives a pulse on the statusline. The ANSI slow-blink
|
|
@@ -44,112 +71,95 @@ function accentGlyph(glyph) {
|
|
|
44
71
|
function blinkingAccentGlyph(glyph) {
|
|
45
72
|
return `\x1b[5;1;${BRAND_RGB}m${glyph}\x1b[0m`;
|
|
46
73
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Build the per-capability palette. `noColor=true` strips every brand
|
|
76
|
+
* ANSI SGR (warning / failed remain because they are SEMANTIC alarms,
|
|
77
|
+
* not brand colour). The unicode glyphs are kept as-is — NO_COLOR
|
|
78
|
+
* (https://no-color.org) addresses ANSI sequences, not UTF-8 chars.
|
|
79
|
+
*/
|
|
80
|
+
function buildPalette(capability, noColor) {
|
|
81
|
+
// Brand SGR helpers, noColor-aware.
|
|
82
|
+
const brand = (text) => noColor || capability === 'ascii' ? text : `${BRAND_SGR_OPEN}${text}${BRAND_SGR_CLOSE}`;
|
|
83
|
+
const brandGlyph = (glyph) => noColor || capability === 'ascii' ? glyph : `${BRAND_SGR_OPEN}${glyph}${BRAND_SGR_CLOSE}`;
|
|
84
|
+
const dimBrand = (text) => {
|
|
85
|
+
if (noColor || capability === 'ascii')
|
|
86
|
+
return text;
|
|
87
|
+
return `\x1b[2;${BRAND_RGB}m${text}\x1b[0m`;
|
|
88
|
+
};
|
|
89
|
+
const blinkBrand = (glyph) => {
|
|
90
|
+
if (noColor || capability === 'ascii')
|
|
91
|
+
return glyph;
|
|
92
|
+
return `\x1b[5;1;${BRAND_RGB}m${glyph}\x1b[0m`;
|
|
93
|
+
};
|
|
94
|
+
// Warning + failed stay semantic — they are alarms, not brand colour.
|
|
95
|
+
// noColor still suppresses them because a tiny log consumer that
|
|
96
|
+
// sets NO_COLOR expects raw text.
|
|
97
|
+
const warning = noColor || capability === 'ascii' ? '!' : '\x1b[33m!\x1b[0m';
|
|
98
|
+
const failed = noColor || capability === 'ascii' ? 'x' : '\x1b[31m✕\x1b[0m';
|
|
99
|
+
if (capability === 'ascii') {
|
|
100
|
+
return {
|
|
101
|
+
active: '*',
|
|
102
|
+
idle: 'o',
|
|
103
|
+
warning,
|
|
104
|
+
inlineSeparator: ' . ',
|
|
105
|
+
trailSeparator: ' -> ',
|
|
106
|
+
idleLabel: 'empty',
|
|
107
|
+
invalidMessage: 'presence unreadable',
|
|
108
|
+
compact: {
|
|
109
|
+
queued: '[', preparing: '+', compacting: '+', verifying: '+',
|
|
110
|
+
completed: '*', failed,
|
|
111
|
+
},
|
|
112
|
+
barFilled: '#',
|
|
113
|
+
barEmpty: '-',
|
|
114
|
+
ratioArrow: '->',
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
active: brandGlyph('●'),
|
|
119
|
+
idle: blinkBrand('○'),
|
|
120
|
+
warning,
|
|
121
|
+
inlineSeparator: brand(' · '),
|
|
122
|
+
trailSeparator: brand(' → '),
|
|
123
|
+
idleLabel: brand('empty'),
|
|
124
|
+
invalidMessage: brand('presence unreadable'),
|
|
96
125
|
compact: {
|
|
97
|
-
queued: '
|
|
98
|
-
preparing: '
|
|
99
|
-
compacting: '
|
|
100
|
-
verifying: '
|
|
101
|
-
completed: '
|
|
102
|
-
failed
|
|
126
|
+
queued: brandGlyph('◐'),
|
|
127
|
+
preparing: brandGlyph('◑'),
|
|
128
|
+
compacting: brandGlyph('◒'),
|
|
129
|
+
verifying: brandGlyph('◓'),
|
|
130
|
+
completed: brandGlyph('✓'),
|
|
131
|
+
failed,
|
|
103
132
|
},
|
|
104
|
-
barFilled: '
|
|
105
|
-
barEmpty: '
|
|
106
|
-
ratioArrow: '
|
|
107
|
-
}
|
|
108
|
-
}
|
|
133
|
+
barFilled: brand('█'),
|
|
134
|
+
barEmpty: dimBrand('░'),
|
|
135
|
+
ratioArrow: dimBrand('→'),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
109
138
|
const BREATHING_GLYPHS_UNICODE = ['●', '◐', '◑', '◒', '◓'];
|
|
110
139
|
const BREATHING_GLYPHS_ASCII = ['*', 'o', '+', '~', '|'];
|
|
111
|
-
const BREATHING_PERIOD_MS =
|
|
112
|
-
/**
|
|
113
|
-
* Bee-tier (1-level sub-role) skills and their full orchestrator
|
|
114
|
-
* parent name. The terminal status line shows both layers when a bee
|
|
115
|
-
* is active: the active bee name plus a `↑<parent-full>` marker
|
|
116
|
-
* pointing at the dispatching orchestrator.
|
|
117
|
-
*/
|
|
118
|
-
const BEE_TO_PARENT = {
|
|
119
|
-
'peaks-prd': 'peaks-code',
|
|
120
|
-
'peaks-rd': 'peaks-code',
|
|
121
|
-
'peaks-qa': 'peaks-code',
|
|
122
|
-
'peaks-ui': 'peaks-code',
|
|
123
|
-
'peaks-sc': 'peaks-code',
|
|
124
|
-
'peaks-txt': 'peaks-code',
|
|
125
|
-
'peaks-final-review': 'peaks-code',
|
|
126
|
-
'peaks-resume': 'peaks-code',
|
|
127
|
-
'peaks-status': 'peaks-code',
|
|
128
|
-
'peaks-test': 'peaks-code',
|
|
129
|
-
'peaks-reviewer': 'peaks-code',
|
|
130
|
-
};
|
|
140
|
+
const BREATHING_PERIOD_MS = 600;
|
|
131
141
|
function pickBreathingGlyph(capability, nowMs) {
|
|
132
142
|
const set = capability === 'ascii' ? BREATHING_GLYPHS_ASCII : BREATHING_GLYPHS_UNICODE;
|
|
133
143
|
const index = Math.floor((nowMs % BREATHING_PERIOD_MS) / (BREATHING_PERIOD_MS / set.length)) % set.length;
|
|
134
144
|
return set[index];
|
|
135
145
|
}
|
|
136
|
-
function renderActiveDot(capability, nowMs) {
|
|
146
|
+
function renderActiveDot(capability, nowMs, noColor) {
|
|
137
147
|
// Brief: the active dot carries the project accent (`#5A65D8` bold)
|
|
138
148
|
// in both colored tiers. The breathing glyph is wrapped in a fresh
|
|
139
149
|
// SGR on every render so the IDE sees a single accent per refresh.
|
|
150
|
+
// NO_COLOR strips the SGR but keeps the glyph (still rotates).
|
|
140
151
|
const glyph = pickBreathingGlyph(capability, nowMs);
|
|
141
|
-
if (capability === 'ascii')
|
|
152
|
+
if (noColor || capability === 'ascii')
|
|
142
153
|
return glyph;
|
|
143
|
-
return
|
|
154
|
+
return `${BRAND_SGR_OPEN}${glyph}${BRAND_SGR_CLOSE}`;
|
|
144
155
|
}
|
|
145
|
-
function brandText(capability) {
|
|
156
|
+
function brandText(capability, noColor) {
|
|
146
157
|
// Brief: brand carries the project accent (`#5A65D8` bold) in both
|
|
147
|
-
// colored tiers. The `ascii` tier
|
|
148
|
-
//
|
|
149
|
-
if (
|
|
150
|
-
return
|
|
151
|
-
}
|
|
152
|
-
return BRAND;
|
|
158
|
+
// colored tiers. The `ascii` tier and NO_COLOR stay plain text so
|
|
159
|
+
// log / file consumers never see escape codes.
|
|
160
|
+
if (noColor || capability === 'ascii')
|
|
161
|
+
return BRAND;
|
|
162
|
+
return `${BRAND_SGR_OPEN}${BRAND}${BRAND_SGR_CLOSE}`;
|
|
153
163
|
}
|
|
154
164
|
/**
|
|
155
165
|
* Default capability when none is supplied. Per the brief: "if backward
|
|
@@ -159,8 +169,8 @@ function brandText(capability) {
|
|
|
159
169
|
* default is the unicode palette (no escape codes).
|
|
160
170
|
*/
|
|
161
171
|
const DEFAULT_CAPABILITY = 'unicode';
|
|
162
|
-
function paletteFor(capability) {
|
|
163
|
-
return
|
|
172
|
+
function paletteFor(capability, noColor) {
|
|
173
|
+
return buildPalette(capability ?? DEFAULT_CAPABILITY, noColor);
|
|
164
174
|
}
|
|
165
175
|
function formatAge(ageMs) {
|
|
166
176
|
if (ageMs === null)
|
|
@@ -181,36 +191,52 @@ function rootLabel(projectRoot) {
|
|
|
181
191
|
* prefix and the project root label. Kept separate so the token layout
|
|
182
192
|
* is obvious at the call site and so each state has a single
|
|
183
193
|
* responsibility.
|
|
194
|
+
*
|
|
195
|
+
* Active-leaf rendering (slice 2026-08-04-rid-005-statusline-dual-skill):
|
|
196
|
+
* when the model carries an `activeLeaf` (an in-flight bee dispatch under
|
|
197
|
+
* the orchestrator), the line surfaces the leaf role alongside the
|
|
198
|
+
* orchestrator skill. The render priorities are:
|
|
199
|
+
*
|
|
200
|
+
* - activeLeaf === null → `${skill}` (current behavior)
|
|
201
|
+
* - activeLeaf.pendingCount === 1 → `${leaf} | ${skill}`
|
|
202
|
+
* - activeLeaf.pendingCount > 1 → `${leaf} (+${N-1}) | ${skill}`
|
|
203
|
+
*
|
|
204
|
+
* The orchestrator skill itself is rendered with its mode token; the leaf
|
|
205
|
+
* role is rendered without a mode (the leaf does not own the mode state —
|
|
206
|
+
* the orchestrator does). The 14→1 bee skill mapping that previously
|
|
207
|
+
* forced every bee role to render with a `↑<parent>` marker was removed
|
|
208
|
+
* in this slice; the dual-skill layout above replaces it.
|
|
184
209
|
*/
|
|
185
|
-
function renderActive(presence, palette, nowMs, capability) {
|
|
210
|
+
function renderActive(presence, palette, nowMs, capability, noColor, activeLeaf) {
|
|
186
211
|
if (!presence) {
|
|
187
212
|
return `${palette.idle} ${palette.idleLabel}`;
|
|
188
213
|
}
|
|
189
214
|
const attentionLabel = isAttentionGate(presence.gate);
|
|
190
215
|
if (attentionLabel !== null) {
|
|
191
216
|
// Attention gate — surface the warning glyph + human-readable label.
|
|
192
|
-
return `${palette.warning} ${presence.skill}${palette.inlineSeparator}${attentionLabel}`;
|
|
217
|
+
return `${palette.warning} ${brandRun(presence.skill, noColor, capability)}${palette.inlineSeparator}${brandRun(attentionLabel, noColor, capability)}`;
|
|
193
218
|
}
|
|
194
219
|
const skill = presence.skill;
|
|
195
|
-
const dot = renderActiveDot(capability, nowMs);
|
|
196
|
-
const beeParent = BEE_TO_PARENT[skill];
|
|
220
|
+
const dot = renderActiveDot(capability, nowMs, noColor);
|
|
197
221
|
const modeToken = typeof presence.mode === 'string' && presence.mode.length > 0
|
|
198
|
-
? ` [${presence.mode}]
|
|
222
|
+
? brandRun(` [${presence.mode}]`, noColor, capability)
|
|
199
223
|
: '';
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
224
|
+
// Dual-skill layout: leaf role (in-flight bee) + orchestrator skill.
|
|
225
|
+
if (activeLeaf !== null) {
|
|
226
|
+
const leaf = brandRun(activeLeaf.role, noColor, capability);
|
|
227
|
+
const tail = activeLeaf.pendingCount > 1
|
|
228
|
+
? ` ${brandRun(`(+${activeLeaf.pendingCount - 1})`, noColor, capability)}`
|
|
229
|
+
: '';
|
|
230
|
+
const sep = brandRun(' | ', noColor, capability);
|
|
231
|
+
return `${dot} ${leaf}${tail}${sep}${brandRun(skill, noColor, capability)}${modeToken}`;
|
|
206
232
|
}
|
|
207
|
-
return `${dot} ${skill}${modeToken}`;
|
|
233
|
+
return `${dot} ${brandRun(skill, noColor, capability)}${modeToken}`;
|
|
208
234
|
}
|
|
209
|
-
function renderStale(presence, ageMs, palette) {
|
|
235
|
+
function renderStale(presence, ageMs, palette, capability, noColor) {
|
|
210
236
|
const skill = presence?.skill ?? 'unknown';
|
|
211
237
|
const age = formatAge(ageMs);
|
|
212
|
-
const ageSuffix = age ? `${palette.inlineSeparator}${age}` : '';
|
|
213
|
-
return `${palette.warning} ${skill}${ageSuffix}`;
|
|
238
|
+
const ageSuffix = age ? `${palette.inlineSeparator}${brandRun(age, noColor, capability)}` : '';
|
|
239
|
+
return `${palette.warning} ${brandRun(skill, noColor, capability)}${ageSuffix}`;
|
|
214
240
|
}
|
|
215
241
|
function renderInvalid(palette) {
|
|
216
242
|
return `${palette.warning} ${palette.invalidMessage}`;
|
|
@@ -339,6 +365,18 @@ function renderCompact(state, palette) {
|
|
|
339
365
|
* is a logger, a file, or any non-interactive sink, and only enables ANSI
|
|
340
366
|
* under an explicit supported condition (TTY + no env veto).
|
|
341
367
|
*/
|
|
368
|
+
/**
|
|
369
|
+
* NO_COLOR (https://no-color.org) is the cross-industry signal that NO
|
|
370
|
+
* ANSI escape sequences should be emitted. The unicode/ansi-unicode
|
|
371
|
+
* tiers honour it by stripping every brand SGR while keeping the UTF-8
|
|
372
|
+
* glyphs (●, █, ░, ◐, …) — NO_COLOR addresses ANSI specifically.
|
|
373
|
+
* PEAKS_STATUSLINE_ASCII is the stricter override that also drops the
|
|
374
|
+
* unicode glyphs; both can coexist and the ASCII override wins.
|
|
375
|
+
*/
|
|
376
|
+
function isNoColor(env) {
|
|
377
|
+
const v = env['NO_COLOR'];
|
|
378
|
+
return typeof v === 'string' && v.length > 0 && v !== '0' && v.toLowerCase() !== 'false';
|
|
379
|
+
}
|
|
342
380
|
export function resolveStatusLineCapability(input) {
|
|
343
381
|
if (input.forced !== undefined) {
|
|
344
382
|
return input.forced;
|
|
@@ -350,12 +388,158 @@ export function resolveStatusLineCapability(input) {
|
|
|
350
388
|
if (typeof asciiFlag === 'string' && (asciiFlag === '1' || asciiFlag === 'true' || asciiFlag === 'yes')) {
|
|
351
389
|
return 'ascii';
|
|
352
390
|
}
|
|
353
|
-
// NO_COLOR is
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
// whether or not the consumer is a TTY.
|
|
391
|
+
// NO_COLOR is honoured at render time (see {@link renderStatusLine});
|
|
392
|
+
// capability stays unicode so the UTF-8 glyphs survive — only the
|
|
393
|
+
// brand SGR is suppressed.
|
|
357
394
|
return input.isTTY ? 'ansi-unicode' : 'unicode';
|
|
358
395
|
}
|
|
396
|
+
export function isNoColorEnv(env) {
|
|
397
|
+
return isNoColor(env);
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Marquee scan band — a single-pass light band that sweeps left ↔ right
|
|
401
|
+
* across the entire status line on a 0.4 s round trip. The band's
|
|
402
|
+
* foreground color is `#E0E0E0` (off-white) with `1;` (bold) — see
|
|
403
|
+
* {@link HIGHLIGHT_SGR_OPEN}. Cells OUTSIDE the band keep their
|
|
404
|
+
* original SGR (brand purple or semantic warning/failed); only cells
|
|
405
|
+
* INSIDE the band are temporarily re-painted to the highlight color.
|
|
406
|
+
*
|
|
407
|
+
* Why it does NOT use SGR 7 (reverse video):
|
|
408
|
+
* - Reverse video inverts BOTH background and foreground, which on a
|
|
409
|
+
* `#5A65D8` background swaps to a white background — visually that
|
|
410
|
+
* reads as a solid white block instead of a scan band.
|
|
411
|
+
* - The off-white foreground + bold keeps the original background
|
|
412
|
+
* visible behind each glyph, so the band reads as "lit text" not
|
|
413
|
+
* as a solid stripe — closer to the CSDN-style scan-band reference
|
|
414
|
+
* image.
|
|
415
|
+
*
|
|
416
|
+
* Phase formula:
|
|
417
|
+
* phase ∈ [0, 1) over MARQUEE_PERIOD_MS
|
|
418
|
+
* sweep = phase < 0.5 ? phase * 2 : (1 - phase) * 2 ← triangle wave 0→1→0
|
|
419
|
+
* center = sweep * (width - 1)
|
|
420
|
+
* band covers visible-cell range [center - half, center + half]
|
|
421
|
+
*
|
|
422
|
+
* When the band would land outside the visible range (center ≤ half
|
|
423
|
+
* OR center ≥ width-1-half) it is clipped — the band never appears
|
|
424
|
+
* off-line. At `nowMs = 0` the band sits at the LEFT edge covering
|
|
425
|
+
* cells [0, bandWidth); this is a deliberate deterministic anchor
|
|
426
|
+
* for tests that pin `withPinnedClock(0, ...)`.
|
|
427
|
+
*
|
|
428
|
+
* ASCII tier: skipped — there are no SGR codes to inject.
|
|
429
|
+
*/
|
|
430
|
+
const MARQUEE_PERIOD_MS = 400;
|
|
431
|
+
const MARQUEE_BAND_WIDTH = 2;
|
|
432
|
+
/**
|
|
433
|
+
* Visible-character width of an ANSI-bearing string. Skips every
|
|
434
|
+
* `\x1b[...m` escape so the count reflects what the terminal paints,
|
|
435
|
+
* not the byte length on the wire.
|
|
436
|
+
*/
|
|
437
|
+
export function visibleCharWidth(s) {
|
|
438
|
+
let w = 0;
|
|
439
|
+
for (let i = 0; i < s.length; i++) {
|
|
440
|
+
if (s[i] === '\x1b' && s[i + 1] === '[') {
|
|
441
|
+
const m = s.indexOf('m', i + 2);
|
|
442
|
+
if (m !== -1) {
|
|
443
|
+
i = m;
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
w++;
|
|
448
|
+
}
|
|
449
|
+
return w;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Split an ANSI string into a flat token stream. Escape sequences are
|
|
453
|
+
* grouped as `\x1b[...m` (single token, kind='esc'); everything else
|
|
454
|
+
* is grouped as a single 'text' run. Used by {@link applyMarquee} to
|
|
455
|
+
* locate visible-cell ranges inside an SGR-bearing string.
|
|
456
|
+
*/
|
|
457
|
+
export function tokenizeAnsi(s) {
|
|
458
|
+
const out = [];
|
|
459
|
+
let i = 0;
|
|
460
|
+
let buf = '';
|
|
461
|
+
const flush = () => {
|
|
462
|
+
if (buf.length > 0) {
|
|
463
|
+
out.push({ kind: 'text', value: buf });
|
|
464
|
+
buf = '';
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
while (i < s.length) {
|
|
468
|
+
if (s[i] === '\x1b' && s[i + 1] === '[') {
|
|
469
|
+
const m = s.indexOf('m', i + 2);
|
|
470
|
+
if (m !== -1) {
|
|
471
|
+
flush();
|
|
472
|
+
out.push({ kind: 'esc', value: s.slice(i, m + 1) });
|
|
473
|
+
i = m + 1;
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
buf += s[i];
|
|
478
|
+
i++;
|
|
479
|
+
}
|
|
480
|
+
flush();
|
|
481
|
+
return out;
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Apply the marquee highlight band to an ANSI-bearing string. Pure.
|
|
485
|
+
* - `bandStart` / `bandEnd` are visible-cell indices (0-based).
|
|
486
|
+
* - Cells in [bandStart, bandEnd] receive a `\x1b[1;38;2;224;224;224m`
|
|
487
|
+
* prefix; cells after the band receive the closing `\x1b[0m` reset
|
|
488
|
+
* so the rest of the line keeps its original SGR.
|
|
489
|
+
* - Empty strings and zero-width bands return the input unchanged.
|
|
490
|
+
*/
|
|
491
|
+
export function applyMarqueeHighlight(s, bandStart, bandEnd) {
|
|
492
|
+
if (s.length === 0)
|
|
493
|
+
return s;
|
|
494
|
+
if (bandEnd < 0 || bandStart > bandEnd)
|
|
495
|
+
return s;
|
|
496
|
+
const tokens = tokenizeAnsi(s);
|
|
497
|
+
const parts = [];
|
|
498
|
+
let visibleIdx = 0;
|
|
499
|
+
let inside = false;
|
|
500
|
+
for (const tok of tokens) {
|
|
501
|
+
if (tok.kind === 'esc') {
|
|
502
|
+
parts.push(tok.value);
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
for (let k = 0; k < tok.value.length; k++) {
|
|
506
|
+
const cellPos = visibleIdx + k;
|
|
507
|
+
const inBand = cellPos >= bandStart && cellPos <= bandEnd;
|
|
508
|
+
if (inBand && !inside) {
|
|
509
|
+
parts.push(HIGHLIGHT_SGR_OPEN);
|
|
510
|
+
inside = true;
|
|
511
|
+
}
|
|
512
|
+
else if (!inBand && inside) {
|
|
513
|
+
parts.push('\x1b[0m');
|
|
514
|
+
inside = false;
|
|
515
|
+
}
|
|
516
|
+
parts.push(tok.value[k]);
|
|
517
|
+
}
|
|
518
|
+
visibleIdx += tok.value.length;
|
|
519
|
+
}
|
|
520
|
+
if (inside)
|
|
521
|
+
parts.push('\x1b[0m');
|
|
522
|
+
return parts.join('');
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Compute the marquee band for a string at time `nowMs` and re-emit
|
|
526
|
+
* the string with the band applied. ASCII tier is a pass-through (no
|
|
527
|
+
* SGR to inject) — the scanner has no visible effect on plain text.
|
|
528
|
+
*/
|
|
529
|
+
export function applyMarquee(s, nowMs, capability) {
|
|
530
|
+
if (capability === 'ascii')
|
|
531
|
+
return s;
|
|
532
|
+
const width = visibleCharWidth(s);
|
|
533
|
+
if (width === 0)
|
|
534
|
+
return s;
|
|
535
|
+
const phase = (nowMs % MARQUEE_PERIOD_MS) / MARQUEE_PERIOD_MS;
|
|
536
|
+
const sweep = phase < 0.5 ? phase * 2 : (1 - phase) * 2;
|
|
537
|
+
const center = Math.round(sweep * (width - 1));
|
|
538
|
+
const halfBand = Math.floor(MARQUEE_BAND_WIDTH / 2);
|
|
539
|
+
const bandStart = Math.max(0, center - halfBand);
|
|
540
|
+
const bandEnd = Math.min(width - 1, center + halfBand);
|
|
541
|
+
return applyMarqueeHighlight(s, bandStart, bandEnd);
|
|
542
|
+
}
|
|
359
543
|
/**
|
|
360
544
|
* Render the status line. Pure — no I/O, no side effects. The output is
|
|
361
545
|
* a single short line suitable for the bottom-of-terminal status bar.
|
|
@@ -367,34 +551,59 @@ export function resolveStatusLineCapability(input) {
|
|
|
367
551
|
* Compact-state precedence: when `model.compact.kind !== 'none'` the
|
|
368
552
|
* compact bar replaces the active/idle/stale content. The C1 baseline
|
|
369
553
|
* line is preserved when the compact state is `none`.
|
|
554
|
+
*
|
|
555
|
+
* The full line is wrapped by {@link applyMarquee} so the brand-purple
|
|
556
|
+
* text + non-focal bar cells share a single colour surface while the
|
|
557
|
+
* scanner band paints its `#E0E0E0` highlight over a moving slice.
|
|
558
|
+
*
|
|
559
|
+
* Idle suppression: the marquee is OFF when `model.state === 'idle'`
|
|
560
|
+
* (and no compact state is active). Idle keeps the slow-blink `○`
|
|
561
|
+
* as the only motion — the user is asking whether the harness is
|
|
562
|
+
* running a skill; the scan band would compete for attention and
|
|
563
|
+
* obscure that question. Active / stale / invalid-presence / compact
|
|
564
|
+
* states all keep the marquee.
|
|
370
565
|
*/
|
|
371
|
-
export function renderStatusLine(model, options) {
|
|
566
|
+
export function renderStatusLine(model, options, env) {
|
|
372
567
|
const capability = options?.capability ?? DEFAULT_CAPABILITY;
|
|
373
|
-
const
|
|
568
|
+
const noColor = env !== undefined ? isNoColor(env) : false;
|
|
569
|
+
const palette = paletteFor(capability, noColor);
|
|
374
570
|
const root = rootLabel(model.projectRoot);
|
|
375
571
|
const rootSuffix = root ? `${palette.trailSeparator}${root}` : '';
|
|
376
|
-
const brand = brandText(capability);
|
|
572
|
+
const brand = brandText(capability, noColor);
|
|
377
573
|
// Breathing pulse: key off a single wall-clock read at the start of
|
|
378
574
|
// render so the output is deterministic for a given invocation. The
|
|
379
575
|
// glyph remains a single cell so total visible width is constant.
|
|
380
576
|
const nowMs = Date.now();
|
|
381
577
|
const compactSegment = renderCompact(model.compact, palette);
|
|
382
|
-
|
|
578
|
+
let line;
|
|
579
|
+
const hasCompact = compactSegment.length > 0;
|
|
580
|
+
if (hasCompact) {
|
|
383
581
|
// Compact state replaces the active / stale / idle skill content.
|
|
384
582
|
// `invalid-presence` still surfaces its own diagnostic when compact
|
|
385
583
|
// is also `invalid` (the compact diagnostic wins, since it's the
|
|
386
584
|
// more recent failure mode).
|
|
387
|
-
|
|
585
|
+
line = `${brand} ${compactSegment}${rootSuffix}`;
|
|
388
586
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
587
|
+
else {
|
|
588
|
+
switch (model.state) {
|
|
589
|
+
case 'active':
|
|
590
|
+
line = `${brand} ${renderActive(model.presence, palette, nowMs, capability, noColor, model.activeLeaf)}${rootSuffix}`;
|
|
591
|
+
break;
|
|
592
|
+
case 'stale':
|
|
593
|
+
line = `${brand} ${renderStale(model.presence, model.ageMs, palette, capability, noColor)}${rootSuffix}`;
|
|
594
|
+
break;
|
|
595
|
+
case 'invalid-presence':
|
|
596
|
+
line = `${brand} ${renderInvalid(palette)}${rootSuffix}`;
|
|
597
|
+
break;
|
|
598
|
+
case 'idle':
|
|
599
|
+
default:
|
|
600
|
+
line = `${brand} ${renderIdle(palette)}${rootSuffix}`;
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
399
603
|
}
|
|
604
|
+
// Marquee is OFF for idle (and only idle). Compact states always
|
|
605
|
+
// carry the band because the compact bar IS the headline. NO_COLOR
|
|
606
|
+
// skips the band entirely — there's no SGR to inject.
|
|
607
|
+
const shouldMarquee = !noColor && (hasCompact || model.state !== 'idle');
|
|
608
|
+
return shouldMarquee ? applyMarquee(line, nowMs, capability) : line;
|
|
400
609
|
}
|
|
@@ -6,6 +6,7 @@ export type StatusLineStdin = {
|
|
|
6
6
|
};
|
|
7
7
|
cwd?: string;
|
|
8
8
|
session_id?: string;
|
|
9
|
+
caller_id?: string;
|
|
9
10
|
};
|
|
10
11
|
export type StatusLineState = 'active' | 'idle' | 'stale' | 'invalid-presence';
|
|
11
12
|
export type StatusLinePresence = {
|
|
@@ -15,12 +16,17 @@ export type StatusLinePresence = {
|
|
|
15
16
|
setAt?: string;
|
|
16
17
|
claudeSessionId?: string;
|
|
17
18
|
};
|
|
19
|
+
export type StatusLineActiveLeaf = {
|
|
20
|
+
role: string;
|
|
21
|
+
pendingCount: number;
|
|
22
|
+
};
|
|
18
23
|
export type StatusLineModel = {
|
|
19
24
|
state: StatusLineState;
|
|
20
25
|
projectRoot: string | null;
|
|
21
26
|
presence: StatusLinePresence | null;
|
|
22
27
|
ageMs: number | null;
|
|
23
28
|
compact: CompactStatuslineState;
|
|
29
|
+
activeLeaf: StatusLineActiveLeaf | null;
|
|
24
30
|
};
|
|
25
31
|
export declare function parseStatusLineStdin(raw: string): StatusLineStdin | null;
|
|
26
32
|
export declare function buildStatusLineModel(stdin: StatusLineStdin | null, nowMs: number): StatusLineModel;
|