@litfamily/litgrok 1.0.24 → 1.0.26
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/.grok/hooks/auto-handoff.mjs +1 -1
- package/.grok/hooks/lit-status-line.mjs +414 -269
- package/.grok/hooks/plan-progress.mjs +11 -2
- package/.grok/hooks/record-passive-event.mjs +20 -9
- package/.grok/hooks/workflow-stage.mjs +48 -13
- package/.grok/rules/00-litgrok.md +21 -12
- package/.grok/skills/autoconference/SKILL.md +3 -1
- package/.grok/skills/autoresearch/SKILL.md +3 -1
- package/.grok/skills/browser-drive/SKILL.md +3 -1
- package/.grok/skills/browser-drive/scripts/capability-probe.mjs +2 -2
- package/.grok/skills/comment-checker/SKILL.md +3 -1
- package/.grok/skills/debugging/SKILL.md +3 -1
- package/.grok/skills/deep-interview/SKILL.md +4 -1
- package/.grok/skills/frontend-ui-ux/scripts/probe.mjs +3 -2
- package/.grok/skills/lit-brainstorm/SKILL.md +162 -0
- package/.grok/skills/lit-brainstorm/references/converge-and-score.md +85 -0
- package/.grok/skills/lit-brainstorm/references/handoff-and-notes.md +66 -0
- package/.grok/skills/lit-brainstorm/references/idea-lane-packet.md +58 -0
- package/.grok/skills/lit-brainstorm/references/idea-lenses.md +60 -0
- package/.grok/skills/lit-diagram-drawer/scripts/diagram-metrics.mjs +1 -1
- package/.grok/skills/lit-diagram-drawer/scripts/export.mjs +6 -6
- package/.grok/skills/lit-diagram-drawer/scripts/renderer-status.mjs +3 -3
- package/.grok/skills/lit-docx/SKILL.md +3 -1
- package/.grok/skills/lit-docx/scripts/convert_md_to_pdf.py +7 -4
- package/.grok/skills/lit-docx/scripts/docx_gate.py +5 -2
- package/.grok/skills/lit-docx/scripts/pdf_fonts.py +63 -0
- package/.grok/skills/lit-docx/scripts/run.mjs +3 -5
- package/.grok/skills/lit-docx/templates/latex/acs/template.tex +5 -0
- package/.grok/skills/lit-docx/templates/latex/elsevier/template.tex +5 -0
- package/.grok/skills/lit-docx/templates/latex/ieee/template.tex +5 -0
- package/.grok/skills/lit-docx/templates/latex/korean-generic/template.tex +5 -0
- package/.grok/skills/lit-docx/templates/latex/nature/template.tex +5 -0
- package/.grok/skills/lit-handoff/SKILL.md +3 -1
- package/.grok/skills/lit-humanizer/scripts/extract.mjs +6 -3
- package/.grok/skills/lit-humanizer/scripts/test-fixtures.mjs +3 -1
- package/.grok/skills/lit-plan/SKILL.md +2 -0
- package/.grok/skills/lit-pptx/SKILL.md +3 -1
- package/.grok/skills/lit-pptx/scripts/deps.cjs +18 -8
- package/.grok/skills/lit-pptx/scripts/proc.cjs +192 -0
- package/.grok/skills/lit-pptx/scripts/run.mjs +3 -5
- package/.grok/skills/lit-recap/SKILL.md +3 -1
- package/.grok/skills/lit-typographic-motion/SKILL.md +9 -6
- package/.grok/skills/lit-typographic-motion/THIRD_PARTY_NOTICES +3 -0
- package/.grok/skills/lit-typographic-motion/fonts/JetBrainsMono-OFL.txt +93 -0
- package/.grok/skills/lit-typographic-motion/fonts/JetBrainsMono-wght-latin.woff2 +0 -0
- package/.grok/skills/lit-typographic-motion/fonts/pins.json +15 -0
- package/.grok/skills/lit-typographic-motion/recipes/negatives/card-scenes/expected.json +16 -0
- package/.grok/skills/lit-typographic-motion/recipes/negatives/card-scenes/index.html +36 -0
- package/.grok/skills/lit-typographic-motion/recipes/negatives/reset-sweep/expected.json +16 -0
- package/.grok/skills/lit-typographic-motion/recipes/negatives/reset-sweep/index.html +24 -0
- package/.grok/skills/lit-typographic-motion/recipes/negatives/still-glow/expected.json +17 -0
- package/.grok/skills/lit-typographic-motion/recipes/negatives/still-glow/index.html +25 -0
- package/.grok/skills/lit-typographic-motion/recipes/negatives/title-slam/expected.json +17 -0
- package/.grok/skills/lit-typographic-motion/recipes/negatives/title-slam/index.html +37 -0
- package/.grok/skills/lit-typographic-motion/recipes/orbit-loop/expected.json +67 -0
- package/.grok/skills/lit-typographic-motion/recipes/orbit-loop/index.html +119 -0
- package/.grok/skills/lit-typographic-motion/recipes/painted-light/expected.json +95 -0
- package/.grok/skills/lit-typographic-motion/recipes/painted-light/index.html +192 -0
- package/.grok/skills/lit-typographic-motion/recipes/weight-tide/expected.json +67 -0
- package/.grok/skills/lit-typographic-motion/recipes/weight-tide/index.html +85 -0
- package/.grok/skills/lit-typographic-motion/references/cover.md +380 -0
- package/.grok/skills/lit-typographic-motion/references/craft-loop.md +4 -0
- package/.grok/skills/lit-typographic-motion/references/runtime.md +14 -3
- package/.grok/skills/lit-typographic-motion/references/stage.md +2 -1
- package/.grok/skills/lit-typographic-motion/references/treatment.md +5 -0
- package/.grok/skills/lit-typographic-motion/requirements-audio.txt +3 -3
- package/.grok/skills/lit-typographic-motion/scripts/lib/cache.mjs +1 -1
- package/.grok/skills/lit-typographic-motion/scripts/lib/chrome.mjs +14 -4
- package/.grok/skills/lit-typographic-motion/scripts/lib/constants.mjs +39 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/cover-assets.mjs +88 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/cover-capture.mjs +144 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/cover-gate.mjs +312 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/cover-image.mjs +111 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/cover-pool.mjs +62 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/cover-sheet.mjs +73 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/cover-treatment.mjs +104 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/cover-webp.mjs +134 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/cover.mjs +347 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/encode.mjs +1 -1
- package/.grok/skills/lit-typographic-motion/scripts/lib/gate.mjs +2 -2
- package/.grok/skills/lit-typographic-motion/scripts/lib/look.mjs +99 -7
- package/.grok/skills/lit-typographic-motion/scripts/lib/render.mjs +8 -11
- package/.grok/skills/lit-typographic-motion/scripts/lib/sound.mjs +1 -1
- package/.grok/skills/lit-typographic-motion/scripts/lib/stage.mjs +3 -1
- package/.grok/skills/lit-typographic-motion/scripts/lib/treatment.mjs +1 -0
- package/.grok/skills/lit-typographic-motion/scripts/motion.mjs +45 -3
- package/.grok/skills/lit-typographic-motion/scripts/prewarm.mjs +35 -22
- package/.grok/skills/lit-typographic-motion/scripts/stage/cover-init.js +44 -0
- package/.grok/skills/lit-typographic-motion/scripts/stage/loop-kit.js +96 -0
- package/.grok/skills/litgoal/SKILL.md +14 -2
- package/.grok/skills/litgrok/SKILL.md +30 -7
- package/.grok/skills/litresearch/SKILL.md +3 -1
- package/.grok/skills/litwork/SKILL.md +12 -1
- package/.grok/skills/lsp/SKILL.md +3 -1
- package/.grok/skills/lsp-setup/SKILL.md +3 -1
- package/.grok/skills/lsp-setup/scripts/lsp-server-table.mjs +3 -3
- package/.grok/skills/readme-studio/templates/hyperframes/package-lock.json +3 -3
- package/.grok/skills/readme-studio/templates/remotion/package-lock.json +3 -3
- package/.grok/skills/refactor/SKILL.md +3 -1
- package/.grok/skills/review-work/SKILL.md +20 -26
- package/.grok/skills/rules/SKILL.md +3 -1
- package/.grok/skills/start-work/SKILL.md +1 -1
- package/.grok/skills/structural-search/SKILL.md +3 -1
- package/.grok/skills/wikify/SKILL.md +2 -0
- package/.grok-plugin/plugin.json +2 -1
- package/CHANGELOG.md +18 -0
- package/README.md +12 -11
- package/README_ko-KR.md +12 -11
- package/bin/litgrok.mjs +7 -2
- package/bin/status-line-config.mjs +26 -14
- package/docs/assets/cover-motion-still.webp +0 -0
- package/docs/assets/cover-motion.webp +0 -0
- package/docs/assets/cover.webp +0 -0
- package/docs/assets/readme/README.md +5 -5
- package/docs/assets/readme/badge-version.svg +1 -1
- package/docs/assets/skills/lit-brainstorm.webp +0 -0
- package/docs/reference.md +18 -8
- package/docs/reference_ko-KR.md +19 -8
- package/package.json +3 -2
- package/plugin.json +1 -1
|
@@ -9,12 +9,19 @@ import { readHudRecordForCwd, writeContextRecord } from './litgrok-hud-state.mjs
|
|
|
9
9
|
import { STAGES, readWorkflowState } from './workflow-stage.mjs';
|
|
10
10
|
|
|
11
11
|
const MAX_INPUT_BYTES = 64 * 1024;
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
// The family look: ONE LitGrok accent, flame orange (255 99 55, LitGrok's tick in the v7 armory), for all
|
|
13
|
+
// ordinary text and separators. Colour means more only in the context bands (yellow at 70%, red at 90%),
|
|
14
|
+
// the pink 🔥 LIT chip, the current-stage chip, a warning, a test verdict and a finished turn.
|
|
15
|
+
const ROLES = {
|
|
16
|
+
accent: [255, 99, 55], dim: [138, 138, 138], empty: [68, 68, 68], rail: [70, 80, 92],
|
|
17
|
+
yellow: [215, 175, 0], red: [215, 95, 95], green: [95, 175, 95], ink: [11, 15, 20], pink: [255, 45, 149],
|
|
18
|
+
};
|
|
19
|
+
// The family mark: L, I and T on pink, violet and cyan.
|
|
20
|
+
const BRAND_STOPS = [[255, 45, 149], [128, 137, 202], [0, 229, 255]];
|
|
14
21
|
// Grok cuts each line at 1024 characters, escapes included.
|
|
15
22
|
const MAX_LINE_CHARACTERS = 1024;
|
|
16
23
|
const RECORD_SCHEMA = 'litgrok.hook-event/v1';
|
|
17
|
-
// The
|
|
24
|
+
// The card reads only the end of the session ledger, enough for a few hundred records.
|
|
18
25
|
const LEDGER_TAIL_BYTES = 128 * 1024;
|
|
19
26
|
const COMPLETE_SHOWN_MS = 20 * 1000;
|
|
20
27
|
const HANDOFF_NEAR_POINTS = 10;
|
|
@@ -22,25 +29,28 @@ const CLOCK_SKEW_MS = 5 * 1000;
|
|
|
22
29
|
// A turn the ledger has not heard from for this long is no longer shown as running.
|
|
23
30
|
const LEDGER_TURN_QUIET_MS = 10 * 60 * 1000;
|
|
24
31
|
const TOOL_EVENTS = new Set(['PostToolUse', 'PostToolUseFailure']);
|
|
25
|
-
// [background, foreground] of each pill.
|
|
26
|
-
const PILLS = {
|
|
27
|
-
brand: [[255, 45, 149], [18, 6, 12]],
|
|
28
|
-
running: [[10, 166, 194], [0, 19, 24]],
|
|
29
|
-
complete: [[29, 77, 54], [184, 245, 207]],
|
|
30
|
-
warning: [[90, 58, 8], [255, 210, 138]],
|
|
31
|
-
};
|
|
32
|
-
const TEXT = { dim: [139, 148, 158], amber: [255, 175, 60], green: [107, 219, 143] };
|
|
33
|
-
const GLYPHS = {
|
|
34
|
-
basic: { running: '●', complete: '✓', warning: '⚠', timer: '', tools: (n) => `${n} tool${n === 1 ? '' : 's'}`, agents: (n) => `${n} agent${n === 1 ? '' : 's'}` },
|
|
35
|
-
nerd: { running: '\uf111', complete: '\uf00c', warning: '\uf071', timer: '\uf017 ', tools: (n) => `\uf0ad ${n}`, agents: (n) => `\uf0c0 ${n}` },
|
|
36
|
-
};
|
|
37
32
|
const ANSI_16 = [[0, 0, 0], [205, 0, 0], [0, 205, 0], [205, 205, 0], [0, 0, 238], [205, 0, 205], [0, 205, 205], [229, 229, 229],
|
|
38
33
|
[127, 127, 127], [255, 0, 0], [0, 255, 0], [255, 255, 0], [92, 92, 255], [255, 0, 255], [0, 255, 255], [255, 255, 255]];
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
//
|
|
42
|
-
const
|
|
43
|
-
|
|
34
|
+
const BRAILLE = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
35
|
+
const plural = (n, word) => `${n} ${word}${n === 1 ? '' : 's'}`;
|
|
36
|
+
// Every symbol the card draws, per glyph set. `ascii` is for terminals and fonts without box, braille or emoji.
|
|
37
|
+
const GLYPHS = {
|
|
38
|
+
basic: {
|
|
39
|
+
fire: '🔥', tick: '✓', cross: '✗', dot: '·', lit: '━', unlit: '─', run: '●', warn: '⚠', elbow: '└', next: '▸', sep: '│',
|
|
40
|
+
full: '▮', empty: '▯', rail: '▎', ellipsis: '…', spinner: BRAILLE, bar: ['█', '░', ['', '▏', '▎', '▍', '▌', '▋', '▊', '▉']],
|
|
41
|
+
timer: '', tools: (n) => plural(n, 'tool'), agents: (n) => plural(n, 'agent'), git: 'git ',
|
|
42
|
+
},
|
|
43
|
+
nerd: {
|
|
44
|
+
fire: '🔥', tick: '', cross: '', dot: '·', lit: '━', unlit: '─', run: '', warn: '', elbow: '└', next: '▸', sep: '│',
|
|
45
|
+
full: '▮', empty: '▯', rail: '▎', ellipsis: '…', spinner: BRAILLE, bar: ['█', '░', ['', '▏', '▎', '▍', '▌', '▋', '▊', '▉']],
|
|
46
|
+
timer: ' ', tools: (n) => ` ${n}`, agents: (n) => ` ${n}`, git: ' ',
|
|
47
|
+
},
|
|
48
|
+
ascii: {
|
|
49
|
+
fire: '', tick: '+', cross: 'x', dot: '-', lit: '=', unlit: '-', run: '*', warn: '!', elbow: '`', next: '>', sep: '|',
|
|
50
|
+
full: '#', empty: '-', rail: '|', ellipsis: '~', spinner: ['|', '/', '-', '\\'], bar: ['#', '-', ['']],
|
|
51
|
+
timer: '', tools: (n) => plural(n, 'tool'), agents: (n) => plural(n, 'agent'), git: 'git ',
|
|
52
|
+
},
|
|
53
|
+
};
|
|
44
54
|
// Grok's own tool names, shortened to the words people know them by. Any other name shows as sent.
|
|
45
55
|
const TOOL_LABELS = {
|
|
46
56
|
run_terminal_command: 'Bash', read_file: 'Read', search_replace: 'Edit', write: 'Write', grep: 'Grep', list_dir: 'List',
|
|
@@ -95,50 +105,27 @@ function windowSize(value) {
|
|
|
95
105
|
return `${Math.max(1, Math.round(value / 1000))}k`;
|
|
96
106
|
}
|
|
97
107
|
|
|
98
|
-
function
|
|
99
|
-
|
|
100
|
-
const filled = Math.floor(exact);
|
|
101
|
-
const partialBlocks = ['', '▏', '▎', '▍', '▌', '▋', '▊', '▉'];
|
|
102
|
-
const partial = filled < 3 && exact > filled ? Math.max(1, Math.min(7, Math.round((exact - filled) * 8))) : 0;
|
|
103
|
-
return `${'█'.repeat(filled)}${partialBlocks[partial]}${'░'.repeat(3 - filled - (partial > 0 ? 1 : 0))}`;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// A percent Grok did not send shows as ?, never as a guess.
|
|
107
|
-
function contextText(contextWindow) {
|
|
108
|
-
const size = windowSize(contextWindow?.context_window_size);
|
|
109
|
-
const value = contextWindow?.used_percentage;
|
|
110
|
-
if (typeof value !== 'number' || !Number.isFinite(value)) return size ? `ctx ?/${size}` : 'ctx ?';
|
|
111
|
-
const percent = Math.round(Math.max(0, Math.min(100, value)));
|
|
112
|
-
return `ctx [${contextBar(percent)}] ${percent}%${size ? `/${size}` : ''}`;
|
|
108
|
+
function finitePercent(value) {
|
|
109
|
+
return typeof value === 'number' && Number.isFinite(value) ? Math.round(Math.max(0, Math.min(100, value))) : null;
|
|
113
110
|
}
|
|
114
111
|
|
|
115
112
|
function cellWidth(character) {
|
|
116
|
-
if (/\p{Mark}/u.test(character) || character === '
|
|
117
|
-
return /[
|
|
113
|
+
if (/\p{Mark}/u.test(character) || character === '') return 0;
|
|
114
|
+
return /[ᄀ-ᅟ〈〉⺀-가-힣豈-︐-︙︰--⦆¢-₩\u{1f300}-\u{1faff}\u{20000}-\u{3fffd}]/u.test(character) ? 2 : 1;
|
|
118
115
|
}
|
|
119
116
|
|
|
120
117
|
function width(text) {
|
|
121
118
|
return [...text].reduce((total, character) => total + cellWidth(character), 0);
|
|
122
119
|
}
|
|
123
120
|
|
|
124
|
-
function clipCells(text, maximum) {
|
|
121
|
+
function clipCells(text, maximum, ellipsis = '…') {
|
|
122
|
+
if (width(text) <= maximum) return text;
|
|
125
123
|
let result = '';
|
|
126
|
-
let used = 0;
|
|
127
|
-
let truncated = false;
|
|
128
124
|
for (const character of text) {
|
|
129
|
-
|
|
130
|
-
if (used + cells > maximum || result.length + character.length > 78) {
|
|
131
|
-
truncated = true;
|
|
132
|
-
break;
|
|
133
|
-
}
|
|
125
|
+
if (width(result) + cellWidth(character) + width(ellipsis) > maximum) break;
|
|
134
126
|
result += character;
|
|
135
|
-
used += cells;
|
|
136
|
-
}
|
|
137
|
-
if (truncated && maximum > 0) {
|
|
138
|
-
while (result && (width(result) + 1 > maximum || result.length >= 78)) result = [...result].slice(0, -1).join('');
|
|
139
|
-
result += '…';
|
|
140
127
|
}
|
|
141
|
-
return result;
|
|
128
|
+
return maximum >= width(ellipsis) ? `${result.trimEnd()}${ellipsis}` : '';
|
|
142
129
|
}
|
|
143
130
|
|
|
144
131
|
function distance(left, right) {
|
|
@@ -166,37 +153,49 @@ function sgr(rgb, depth, background = false) {
|
|
|
166
153
|
return `\u001b[${background ? 48 : 38};2;${rgb.join(';')}m`;
|
|
167
154
|
}
|
|
168
155
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
|
|
156
|
+
// A painter for one colour depth; depth 0 returns the text untouched.
|
|
157
|
+
function painter(depth) {
|
|
158
|
+
return {
|
|
159
|
+
depth,
|
|
160
|
+
fg: (text, role, bold = false) => (depth && text ? `${bold ? '\u001b[1m' : ''}${sgr(ROLES[role], depth)}${text}\u001b[0m` : text),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// A filled chip on the pink (the brand) or the accent (the current stage) with dark text and a space of
|
|
165
|
+
// padding each side; a Nerd Font gives it rounded caps, and without colour it falls back to brackets.
|
|
166
|
+
function chip(text, background, paint, nerd, open = '(', close = ')') {
|
|
167
|
+
if (!paint.depth) return { plain: `${open}${text}${close}`, painted: `${open}${text}${close}` };
|
|
168
|
+
const fill = `${sgr(ROLES[background], paint.depth, true)}${sgr(ROLES.ink, paint.depth)}\u001b[1m`;
|
|
169
|
+
if (!nerd) return { plain: ` ${text} `, painted: `${fill} ${text} \u001b[0m` };
|
|
170
|
+
const cap = (glyph) => `${sgr(ROLES[background], paint.depth)}${glyph}\u001b[0m`;
|
|
171
|
+
return { plain: `${text}`, painted: `${cap('')}${fill}${text}\u001b[0m${cap('')}` };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Below 70% a gauge keeps the accent; 70% turns it yellow and 90% red, as the family gauges do.
|
|
175
|
+
function bandOf(percent) {
|
|
176
|
+
return percent >= 90 ? 'red' : percent >= 70 ? 'yellow' : 'accent';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function blockBar(percent, cells, glyphs) {
|
|
180
|
+
const [full, empty, partials] = glyphs.bar;
|
|
181
|
+
const exact = (percent * cells) / 100;
|
|
182
|
+
const filled = Math.floor(exact);
|
|
183
|
+
const partial = partials.length > 1 && filled < cells && exact > filled ? Math.max(1, Math.min(partials.length - 1, Math.round((exact - filled) * partials.length))) : 0;
|
|
184
|
+
return { filled: `${full.repeat(filled)}${partials[partial]}`, empty: empty.repeat(cells - filled - (partial > 0 ? 1 : 0)) };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function duration(ms) {
|
|
188
|
+
const seconds = Math.floor(ms / 1000);
|
|
189
|
+
if (seconds < 60) return `${seconds}s`;
|
|
190
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m${String(seconds % 60).padStart(2, '0')}s`;
|
|
191
|
+
return `${Math.floor(seconds / 3600)}h${String(Math.floor(seconds / 60) % 60).padStart(2, '0')}m`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function clock(ms) {
|
|
195
|
+
const seconds = Math.floor(Math.max(0, ms) / 1000);
|
|
196
|
+
const minutes = Math.floor(seconds / 60);
|
|
197
|
+
const rest = String(seconds % 60).padStart(2, '0');
|
|
198
|
+
return minutes < 60 ? `${minutes}:${rest}` : `${Math.floor(minutes / 60)}:${String(minutes % 60).padStart(2, '0')}:${rest}`;
|
|
200
199
|
}
|
|
201
200
|
|
|
202
201
|
// Automatic handoff needs Grok's context percent, which only the status line receives. Nothing is
|
|
@@ -313,25 +312,15 @@ function turnActivity(records, promptId) {
|
|
|
313
312
|
};
|
|
314
313
|
}
|
|
315
314
|
|
|
316
|
-
function duration(ms) {
|
|
317
|
-
const seconds = Math.floor(ms / 1000);
|
|
318
|
-
if (seconds < 60) return `${seconds}s`;
|
|
319
|
-
if (seconds < 3600) return `${Math.floor(seconds / 60)}m${String(seconds % 60).padStart(2, '0')}s`;
|
|
320
|
-
return `${Math.floor(seconds / 3600)}h${String(Math.floor(seconds / 60) % 60).padStart(2, '0')}m`;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
315
|
// Returns [long, short] warning text, or null. A plan-gate hold comes first, then a handoff request,
|
|
324
316
|
// then a context at or near the automatic-handoff percent.
|
|
325
|
-
function warningText(status, activity) {
|
|
317
|
+
function warningText(status, activity, handoff) {
|
|
326
318
|
if (activity?.planGate) return ['plan gate: save plans/<slug>.md', 'plan gate'];
|
|
327
319
|
if (activity?.handoffAsked) return ['handoff requested', 'handoff'];
|
|
328
|
-
const
|
|
329
|
-
if (
|
|
330
|
-
|
|
331
|
-
if (
|
|
332
|
-
const percent = Math.round(Math.max(0, Math.min(100, used)));
|
|
333
|
-
if (percent >= settings.percent) return [`ctx ${percent}% ≥ handoff at ${settings.percent}%`, `ctx ${percent}%≥${settings.percent}%`];
|
|
334
|
-
if (percent >= settings.percent - HANDOFF_NEAR_POINTS) return [`ctx ${percent}% → handoff at ${settings.percent}%`, `ctx ${percent}%→${settings.percent}%`];
|
|
320
|
+
const percent = finitePercent(status?.context_window?.used_percentage);
|
|
321
|
+
if (percent === null || !handoff.active) return null;
|
|
322
|
+
if (percent >= handoff.percent) return [`ctx ${percent}% ≥ handoff at ${handoff.percent}%`, `ctx ${percent}%≥${handoff.percent}%`];
|
|
323
|
+
if (percent >= handoff.percent - HANDOFF_NEAR_POINTS) return [`ctx ${percent}% → handoff at ${handoff.percent}%`, `ctx ${percent}%→${handoff.percent}%`];
|
|
335
324
|
return null;
|
|
336
325
|
}
|
|
337
326
|
|
|
@@ -350,208 +339,351 @@ function turnState(status, activity, now) {
|
|
|
350
339
|
return { running, startedAt: running ? activity.startedAt : null };
|
|
351
340
|
}
|
|
352
341
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
342
|
+
// running, complete (for twenty seconds after a clean stop) or idle, with the turn's timer.
|
|
343
|
+
function phaseOf(now, activity, turn) {
|
|
344
|
+
if (turn.running) return { phase: 'running', timer: turn.startedAt === null ? null : duration(Math.max(0, now - turn.startedAt)) };
|
|
345
|
+
if (activity?.endedAt && !activity.failed && now - activity.endedAt <= COMPLETE_SHOWN_MS && activity.endedAt <= now + CLOCK_SKEW_MS) {
|
|
346
|
+
return { phase: 'complete', timer: duration(Math.max(0, activity.endedAt - activity.startedAt)) };
|
|
347
|
+
}
|
|
348
|
+
return { phase: 'idle', timer: null };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* A piece of a line. `paint` colours the text it is given. A narrow row works down from the highest
|
|
353
|
+
* `drop` or `clip` number (0 never): a drop removes the segment, a clip shortens it through
|
|
354
|
+
* `shrink(cells)`, which returns the text at that width or '' when it cannot get that small. `lead` (and
|
|
355
|
+
* its coloured form `leadPainted`) is what stands before it, instead of the line's separator.
|
|
356
|
+
*/
|
|
357
|
+
const segment = (text, paint = (value) => value, { drop = 0, clip = 0, shrink = null, lead = null, leadPainted = null, minCells = 1 } = {}) => ({
|
|
358
|
+
text, paint, drop, clip, shrink, lead, leadPainted, minCells,
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// Lays one line out of segments; `right` segments sit flush with the row's right edge.
|
|
362
|
+
function layoutLine({ left, right = [], separator, rail, maxCells, ellipsis }) {
|
|
363
|
+
const items = [...left.map((item) => ({ ...item, side: 'left' })), ...right.map((item) => ({ ...item, side: 'right' }))];
|
|
364
|
+
for (;;) {
|
|
365
|
+
const join = (side, painted) => items.filter((item) => item.side === side).map((item, index) => {
|
|
366
|
+
// A segment that comes straight after the first one may take its own lead (`|` after the mark).
|
|
367
|
+
const own = index === 1 && item.firstLead ? item.firstLead : item;
|
|
368
|
+
const lead = index === 0 ? '' : own.lead ?? separator.plain;
|
|
369
|
+
const paintedLead = index === 0 ? '' : own.leadPainted ?? own.lead ?? separator.painted;
|
|
370
|
+
return painted ? `${paintedLead}${item.paint(item.text)}` : `${lead}${item.text}`;
|
|
371
|
+
}).join('');
|
|
372
|
+
const leftPlain = `${rail.plain}${join('left', false)}`;
|
|
373
|
+
const rightPlain = join('right', false);
|
|
374
|
+
const bare = `${rail.painted}${join('left', true)}${join('right', true)}`;
|
|
375
|
+
// The padding that pushes the right side to the edge counts toward Grok's cut as well.
|
|
376
|
+
const gap = rightPlain ? Math.max(2, Math.min(maxCells - width(leftPlain) - width(rightPlain), MAX_LINE_CHARACTERS - bare.length)) : 0;
|
|
377
|
+
const total = width(leftPlain) + gap + width(rightPlain);
|
|
378
|
+
const painted = `${rail.painted}${join('left', true)}${' '.repeat(gap)}${join('right', true)}`;
|
|
379
|
+
if (total <= maxCells && painted.length <= MAX_LINE_CHARACTERS) return painted;
|
|
380
|
+
// The highest drop or clip number goes first; a clipped segment can still be dropped later.
|
|
381
|
+
const steps = items.flatMap((item) => [...(item.drop > 0 ? [{ item, at: item.drop, drop: true }] : []), ...(item.clip > 0 ? [{ item, at: item.clip, drop: false }] : [])]);
|
|
382
|
+
if (steps.length === 0) {
|
|
383
|
+
// Nothing left to drop or shorten: the last segment goes, and the first is kept to the end.
|
|
384
|
+
if (items.length > 1) items.pop();
|
|
385
|
+
else return `${rail.painted}${clipCells(items[0]?.text ?? '', Math.max(0, maxCells - width(rail.plain)), ellipsis)}`;
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
const step = steps.reduce((best, candidate) => (candidate.at > best.at || (candidate.at === best.at && !candidate.drop) ? candidate : best));
|
|
389
|
+
const target = step.item;
|
|
390
|
+
if (step.drop) {
|
|
391
|
+
items.splice(items.indexOf(target), 1);
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
// A segment that cannot get small enough shrinks as far as it goes, and the next step follows.
|
|
395
|
+
const shrink = (cells) => (target.shrink ? target.shrink(cells) : clipCells(target.text, cells, ellipsis));
|
|
396
|
+
if (total > maxCells) {
|
|
397
|
+
const shorter = shrink(Math.max(target.minCells, width(target.text) - (total - maxCells)));
|
|
398
|
+
if (shorter && width(shorter) < width(target.text)) target.text = shorter;
|
|
399
|
+
} else {
|
|
400
|
+
// Escapes count toward Grok's cut too, so a row can be too long while it fits its columns: the
|
|
401
|
+
// segment shrinks until its painted form is short enough.
|
|
402
|
+
const excess = painted.length - MAX_LINE_CHARACTERS;
|
|
403
|
+
const length = target.paint(target.text).length;
|
|
404
|
+
for (let cells = width(target.text) - 1; cells >= target.minCells; cells -= 1) {
|
|
405
|
+
const shorter = shrink(cells);
|
|
406
|
+
if (shorter && (length - target.paint(shorter).length >= excess || cells === target.minCells)) {
|
|
407
|
+
target.text = shorter;
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
target.clip = 0;
|
|
364
413
|
}
|
|
365
|
-
if (state === null && warning === null) return null;
|
|
366
|
-
return { state, timer, warning, activity: running || state === 'complete' ? activity : null };
|
|
367
414
|
}
|
|
368
415
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
416
|
+
// The figures in the family order: model [effort] │ ctx [█▎░] n%/size, then the automatic handoff percent,
|
|
417
|
+
// the session's cost and the Git branch, each left out when Grok did not send it.
|
|
418
|
+
function figureSegments(status, { glyphs, paint, handoff }) {
|
|
419
|
+
const model = plainText(status?.model?.display_name, plainText(status?.model?.id, 'grok'));
|
|
420
|
+
const effort = effortLevel(status?.effort?.level);
|
|
421
|
+
const percent = finitePercent(status?.context_window?.used_percentage);
|
|
422
|
+
const size = windowSize(status?.context_window?.context_window_size);
|
|
423
|
+
const accent = (text) => paint.fg(text, 'accent');
|
|
424
|
+
// A narrow row clips the model name and keeps the effort beside it; the narrowest give it up for the gauge.
|
|
425
|
+
const tail = effort ? ` [${effort}]` : '';
|
|
426
|
+
const segments = [segment(`${model}${tail}`, accent, {
|
|
427
|
+
drop: 0.5, clip: 2, minCells: width(tail) + 1, shrink: (cells) => `${clipCells(model, Math.max(1, cells - width(tail)), glyphs.ellipsis)}${tail}`,
|
|
428
|
+
})];
|
|
429
|
+
if (percent === null) {
|
|
430
|
+
segments.push(segment(`ctx ?${size ? `/${size}` : ''}`, accent));
|
|
431
|
+
} else {
|
|
432
|
+
const band = bandOf(percent);
|
|
433
|
+
const bar = blockBar(percent, 3, glyphs);
|
|
434
|
+
const figure = `${percent}%${size ? `/${size}` : ''}`;
|
|
435
|
+
segments.push(segment(`ctx [${bar.filled}${bar.empty}] ${figure}`, () => (
|
|
436
|
+
`${accent('ctx [')}${paint.fg(bar.filled, band)}${paint.fg(bar.empty, 'empty')}${accent(']')} ${paint.fg(figure, band)}`)));
|
|
375
437
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
const kind = band.warning ? 'warning' : band.state === 'complete' ? 'complete' : 'running';
|
|
382
|
-
const pills = [pill(brand === 'full' ? '🔥 LIT' : '🔥', 'brand', depth, nerd), pill(`${glyphs[kind]} ${clipCells(discipline, skillCells)}`, kind, depth, nerd)];
|
|
383
|
-
const items = [];
|
|
384
|
-
const paint = (text, rgb, bold = false) => (depth ? `${bold ? '\u001b[1m' : ''}${sgr(rgb, depth)}${text}\u001b[0m` : text);
|
|
385
|
-
if (band.warning && warning) items.push({ plain: band.warning[warningIndex], painted: paint(band.warning[warningIndex], TEXT.amber, true) });
|
|
386
|
-
if (band.state === 'complete') items.push({ plain: 'complete', painted: paint('complete', TEXT.green, true) });
|
|
387
|
-
if (band.timer && timer) items.push({ plain: `${glyphs.timer}${band.timer}`, painted: paint(`${glyphs.timer}${band.timer}`, TEXT.dim) });
|
|
388
|
-
if (band.activity && tools) items.push({ plain: glyphs.tools(band.activity.tools), painted: paint(glyphs.tools(band.activity.tools), TEXT.dim) });
|
|
389
|
-
if (band.activity && agents && band.state === 'running' && band.activity.agents > 0) {
|
|
390
|
-
items.push({ plain: glyphs.agents(band.activity.agents), painted: paint(glyphs.agents(band.activity.agents), TEXT.dim) });
|
|
438
|
+
if (handoff.active) segments.push(segment(`handoff@${handoff.percent}%`, accent, { drop: 4 }));
|
|
439
|
+
const cost = status?.cost?.total_cost_usd;
|
|
440
|
+
// Grok's own row hides a cost under half a cent so it never shows a misleading $0.00.
|
|
441
|
+
if (typeof cost === 'number' && Number.isFinite(cost) && cost >= 0.005) {
|
|
442
|
+
segments.push(segment(`$${cost < 100 ? cost.toFixed(2) : Math.round(cost)}`, accent, { drop: 3 }));
|
|
391
443
|
}
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// Narrow rows give up tool calls first, then subagents, the timer, the long warning, the warning itself
|
|
398
|
-
// and the brand's name, and last of all clip the skill name.
|
|
399
|
-
function formatBand(discipline, band, depth, nerd, maxCells) {
|
|
400
|
-
const tiers = [
|
|
401
|
-
{},
|
|
402
|
-
{ tools: false },
|
|
403
|
-
{ tools: false, agents: false },
|
|
404
|
-
{ tools: false, agents: false, timer: false },
|
|
405
|
-
{ tools: false, agents: false, timer: false, warningIndex: 1 },
|
|
406
|
-
{ tools: false, agents: false, timer: false, warningIndex: 1, brand: 'compact' },
|
|
407
|
-
{ tools: false, agents: false, timer: false, warning: false, brand: 'compact' },
|
|
408
|
-
];
|
|
409
|
-
for (const tier of tiers) {
|
|
410
|
-
const row = renderBand(discipline, band, depth, nerd, tier);
|
|
411
|
-
if (width(row.plain) <= maxCells && row.painted.length <= MAX_LINE_CHARACTERS) return row.painted;
|
|
444
|
+
const branch = plainText(status?.workspace?.branch, '');
|
|
445
|
+
if (branch) {
|
|
446
|
+
const worktree = plainText(status?.workspace?.git_worktree, '');
|
|
447
|
+
segments.push(segment(`${glyphs.git}${clipCells(branch, 32, glyphs.ellipsis)}${worktree ? ` ${glyphs.dot} ${clipCells(worktree, 20, glyphs.ellipsis)}` : ''}`, accent, { drop: 5 }));
|
|
412
448
|
}
|
|
413
|
-
|
|
414
|
-
const room = maxCells - width(renderBand('', band, depth, nerd, { ...last, skillCells: 0 }).plain);
|
|
415
|
-
return renderBand(discipline, band, depth, nerd, { ...last, skillCells: Math.max(1, room) }).painted;
|
|
449
|
+
return segments;
|
|
416
450
|
}
|
|
417
451
|
|
|
418
|
-
function
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
452
|
+
function brandText(version, glyphs) {
|
|
453
|
+
return `[${glyphs.fire}LIT${version ? ` v${version}` : ''}]`;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// The family mark: bold, the fire and the brackets in the terminal's own colour, L, I and T on the
|
|
457
|
+
// gradient, the version in its last stop. 256 colours make it bold magenta and 16 colours bold cyan.
|
|
458
|
+
function brandSegment(version, glyphs, paint, options = {}) {
|
|
459
|
+
const paintBrand = (text) => {
|
|
460
|
+
if (!paint.depth) return text;
|
|
461
|
+
if (paint.depth === 256) return `\u001b[0m\u001b[1m\u001b[38;5;201m${text}\u001b[0m`;
|
|
462
|
+
if (paint.depth === 16) return `\u001b[0m\u001b[1m\u001b[36m${text}\u001b[0m`;
|
|
463
|
+
const at = text.indexOf('LIT');
|
|
464
|
+
const letters = [...'LIT'].map((letter, index) => `${sgr(BRAND_STOPS[index], 24)}${letter}`).join('');
|
|
465
|
+
return `\u001b[0m\u001b[1m${text.slice(0, at)}${letters}${sgr(BRAND_STOPS[2], 24)}${text.slice(at + 3)}\u001b[0m`;
|
|
466
|
+
};
|
|
467
|
+
// A narrow row lets the version go before anything else on the mark.
|
|
468
|
+
return segment(brandText(version, glyphs), paintBrand, { ...options, shrink: () => brandText('', glyphs) });
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// The discipline keeps to 24 cells (32 for the longest skill name) so the line stays far below Grok's
|
|
472
|
+
// 1024-character cut.
|
|
473
|
+
function disciplineName(name, glyphs, cells = Infinity) {
|
|
474
|
+
return clipCells(name, Math.min(cells, name === 'lit-scientific-visualization' ? 32 : 24), glyphs.ellipsis);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// The ignition mark of the one-line row: the pink 🔥 LIT chip, then IGNITED · <discipline> 🔥 in the accent.
|
|
478
|
+
// Without colour it reads 🔥 LIT IGNITED · <discipline> 🔥. A narrow row clips the discipline, never the frame.
|
|
479
|
+
function ignitionSegment(discipline, glyphs, paint, nerd, options = {}) {
|
|
480
|
+
const label = glyphs.fire ? `${glyphs.fire} LIT` : 'LIT';
|
|
481
|
+
const brand = chip(label, 'pink', paint, nerd, '', '');
|
|
482
|
+
const build = (name) => `${brand.plain} IGNITED ${glyphs.dot} ${name}${glyphs.fire ? ` ${glyphs.fire}` : ''}`;
|
|
483
|
+
const frame = width(build(''));
|
|
484
|
+
return segment(build(disciplineName(discipline, glyphs)), (text) => `${brand.painted}${paint.fg(text.slice(brand.plain.length), 'accent', true)}`, {
|
|
485
|
+
...options, minCells: frame + 1, shrink: (cells) => build(disciplineName(discipline, glyphs, Math.max(1, cells - frame))),
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Joins the figures to a line: `|` after the mark, `│` between the rest, all in the accent.
|
|
490
|
+
function figureLeads(figures, glyphs, paint, afterMark) {
|
|
491
|
+
return figures.map((item, index) => {
|
|
492
|
+
const lead = ` ${glyphs.sep} `;
|
|
493
|
+
return { ...item, lead, leadPainted: paint.fg(lead, 'accent'), firstLead: afterMark ? { lead: ' | ', leadPainted: paint.fg(' | ', 'accent') } : null };
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// One row: the mark, model [effort] │ ctx, the ignition mark while a discipline is active, then the
|
|
498
|
+
// handoff percent, cost and branch. It is the whole HUD between workflows and in LITGROK_HUD_LINES=1
|
|
499
|
+
// mode. A narrow row gives up the branch, the handoff percent, the cost and the version, then clips the
|
|
500
|
+
// model name and last the discipline.
|
|
501
|
+
function compactRow({ status, discipline, version, glyphs, paint, nerd, handoff, maxCells }) {
|
|
502
|
+
const [model, context, ...extras] = figureSegments(status, { glyphs, paint, handoff });
|
|
503
|
+
const figures = [model, context, ...(discipline ? [ignitionSegment(discipline, glyphs, paint, nerd, { clip: 1 })] : []), ...extras];
|
|
504
|
+
const left = [brandSegment(version, glyphs, paint, { clip: 3 }), ...figureLeads(figures, glyphs, paint, true)];
|
|
505
|
+
return layoutLine({ left, separator: { plain: ' ', painted: ' ' }, rail: { plain: '', painted: '' }, maxCells, ellipsis: glyphs.ellipsis });
|
|
423
506
|
}
|
|
424
507
|
|
|
425
|
-
// The current stage's
|
|
508
|
+
// The current stage's label: its name, the plan's checked tasks while working, and a tick once the
|
|
426
509
|
// stage's turn has ended (for work, only once every task is checked).
|
|
427
|
-
function stageLabel(workflow) {
|
|
428
|
-
if (workflow.stage === 'done') return
|
|
510
|
+
function stageLabel(workflow, glyphs) {
|
|
511
|
+
if (workflow.stage === 'done') return `done ${glyphs.tick}`;
|
|
429
512
|
const plan = workflow.plan;
|
|
430
513
|
const counter = workflow.stage === 'work' && plan?.total > 0 ? ` ${plan.done}/${plan.total}` : '';
|
|
431
514
|
const finished = workflow.ended && (workflow.stage !== 'work' || (plan?.total > 0 && plan.done === plan.total));
|
|
432
|
-
return `${workflow.stage
|
|
515
|
+
return `${workflow.stage}${counter}${finished ? ` ${glyphs.tick}` : ''}`;
|
|
433
516
|
}
|
|
434
517
|
|
|
435
|
-
// The
|
|
436
|
-
//
|
|
437
|
-
//
|
|
438
|
-
//
|
|
439
|
-
|
|
518
|
+
// The stage map as the family card draws it: interview ✓ ━━ plan ✓ ━━ [work 3/7] ━━ review · ── done ·.
|
|
519
|
+
// Stages behind are dim with a tick when the session went through them, the current stage is a chip on the
|
|
520
|
+
// accent, the stages ahead are dim with a dot. Rails into a passed or the current stage are accent `━`, the
|
|
521
|
+
// rail leaving the current stage is a grey `━` and the rails further on are grey `─`. Interview shows only
|
|
522
|
+
// when it happened.
|
|
523
|
+
function mapPieces(workflow, { rail, compact }, glyphs) {
|
|
440
524
|
const shown = STAGES.filter((stage) => stage !== 'interview' || workflow.visited.includes('interview') || workflow.stage === 'interview');
|
|
441
525
|
const current = shown.indexOf(workflow.stage);
|
|
442
|
-
const last = Math.max(1, shown.length - 1);
|
|
443
526
|
const pieces = [];
|
|
444
527
|
shown.forEach((stage, index) => {
|
|
445
528
|
if (index > 0) {
|
|
446
529
|
if (!compact) pieces.push({ text: ' ', role: 'space' });
|
|
447
530
|
for (let cell = 0; cell < rail; cell += 1) {
|
|
448
|
-
|
|
531
|
+
const role = index <= current ? 'lit' : index === current + 1 ? 'leaving' : 'ahead';
|
|
532
|
+
pieces.push({ text: role === 'ahead' ? glyphs.unlit : glyphs.lit, role, rail: [index, cell] });
|
|
449
533
|
}
|
|
450
534
|
if (!compact) pieces.push({ text: ' ', role: 'space' });
|
|
451
535
|
}
|
|
452
|
-
|
|
453
|
-
if (index
|
|
454
|
-
else if (
|
|
455
|
-
else
|
|
456
|
-
else pieces.push({ text: compact ? '-' : stage, role: 'skipped' });
|
|
536
|
+
if (index === current) pieces.push({ text: stageLabel(workflow, glyphs), role: 'current' });
|
|
537
|
+
else if (index > current) pieces.push({ text: compact ? glyphs.dot : `${stage} ${glyphs.dot}`, role: 'dim' });
|
|
538
|
+
else if (workflow.visited.includes(stage)) pieces.push({ text: compact ? glyphs.tick : `${stage} ${glyphs.tick}`, role: 'dim' });
|
|
539
|
+
else pieces.push({ text: compact ? glyphs.unlit : stage, role: 'dim' });
|
|
457
540
|
});
|
|
458
541
|
return { pieces, current };
|
|
459
542
|
}
|
|
460
543
|
|
|
461
|
-
// While a turn runs, one
|
|
462
|
-
//
|
|
463
|
-
function
|
|
464
|
-
const { pieces, current } =
|
|
465
|
-
const
|
|
466
|
-
|
|
544
|
+
// While a turn runs, one pink cell rides the rail into the current stage and moves on at each redraw
|
|
545
|
+
// (Grok redraws about every 300 ms during a turn).
|
|
546
|
+
function renderMap(workflow, tier, { paint, glyphs, nerd, running, now }) {
|
|
547
|
+
const { pieces, current } = mapPieces(workflow, tier, glyphs);
|
|
548
|
+
const pill = (text) => chip(text, 'accent', paint, nerd, '[', ']');
|
|
549
|
+
const plain = pieces.map(({ text, role }) => (role === 'current' ? pill(text).plain : text)).join('');
|
|
550
|
+
if (!paint.depth) return { plain, painted: plain };
|
|
467
551
|
const head = running ? [current > 0 ? current : 1, Math.floor(now / 300) % tier.rail] : null;
|
|
468
|
-
const painted = pieces.map(({ text, role,
|
|
552
|
+
const painted = pieces.map(({ text, role, rail }) => {
|
|
469
553
|
if (role === 'space') return text;
|
|
470
|
-
if (role === '
|
|
471
|
-
|
|
472
|
-
if (
|
|
473
|
-
|
|
474
|
-
if (head && rail[0] === head[0] && rail[1] === head[1]) return `${sgr(RAIL.head, depth)}${text}`;
|
|
475
|
-
return `${sgr(role === 'lit' ? rgb : RAIL.faint, depth)}${text}`;
|
|
554
|
+
if (role === 'current') return pill(text).painted;
|
|
555
|
+
if (role === 'dim') return paint.fg(text, 'dim');
|
|
556
|
+
if (head && rail[0] === head[0] && rail[1] === head[1]) return paint.fg(text, 'pink', true);
|
|
557
|
+
return paint.fg(text, role === 'lit' ? 'accent' : 'rail');
|
|
476
558
|
}).join('');
|
|
477
|
-
return { plain, painted
|
|
559
|
+
return { plain, painted };
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Narrow rows shorten the rails to two cells and one, then drop the stage names, and last of all keep
|
|
563
|
+
// only the current stage.
|
|
564
|
+
function mapLine(workflow, context, rail, maxCells) {
|
|
565
|
+
const room = maxCells - width(rail.plain);
|
|
566
|
+
for (const tier of [{ rail: 4 }, { rail: 2 }, { rail: 1 }, { rail: 1, compact: true }]) {
|
|
567
|
+
const map = renderMap(workflow, tier, context);
|
|
568
|
+
if (width(map.plain) <= room && map.painted.length + rail.painted.length <= MAX_LINE_CHARACTERS) return `${rail.painted}${map.painted}`;
|
|
569
|
+
}
|
|
570
|
+
const label = clipCells(stageLabel(workflow, context.glyphs), Math.max(1, room - 2), context.glyphs.ellipsis);
|
|
571
|
+
return `${rail.painted}${chip(label, 'accent', context.paint, context.nerd, '[', ']').painted}`;
|
|
478
572
|
}
|
|
479
573
|
|
|
480
574
|
function toolLabel(name) {
|
|
481
575
|
return Object.hasOwn(TOOL_LABELS, name) ? TOOL_LABELS[name] : clipCells(plainText(name, 'tool'), 20);
|
|
482
576
|
}
|
|
483
577
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
578
|
+
// The card's left edge: one accent bar per line, so the lines read as one card.
|
|
579
|
+
function railFor(paint, glyphs) {
|
|
580
|
+
if (!paint.depth) return { plain: '', painted: '' };
|
|
581
|
+
return { plain: `${glyphs.rail} `, painted: `${paint.fg(glyphs.rail, 'accent')} ` };
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// The card's title: the mark, the pink 🔥 LIT chip and the discipline, then at the right edge the turn's
|
|
585
|
+
// tool calls and subagents beside its state, as the family card's corner reads (● 1m42s running,
|
|
586
|
+
// ✓ complete, ⚠ a warning, idle).
|
|
587
|
+
function titleLine({ discipline, version, glyphs, paint, nerd, phase, warning, activity, maxCells, rail }) {
|
|
588
|
+
const brand = chip(glyphs.fire ? `${glyphs.fire} LIT` : 'LIT', 'pink', paint, nerd);
|
|
589
|
+
const name = discipline ?? 'workflow';
|
|
590
|
+
const left = [
|
|
591
|
+
brandSegment(version, glyphs, paint, { clip: 3 }),
|
|
592
|
+
{ ...segment(brand.plain, () => brand.painted), lead: ' | ', leadPainted: paint.fg(' | ', 'accent') },
|
|
593
|
+
segment(`${glyphs.dot} ${disciplineName(name, glyphs)}`, (text) => `${paint.fg(glyphs.dot, 'dim')} ${paint.fg(text.slice(glyphs.dot.length + 1), discipline ? 'accent' : 'dim', Boolean(discipline))}`, {
|
|
594
|
+
clip: 0.5, minCells: width(glyphs.dot) + 2, shrink: (cells) => `${glyphs.dot} ${disciplineName(name, glyphs, Math.max(1, cells - width(glyphs.dot) - 1))}`,
|
|
595
|
+
}),
|
|
596
|
+
];
|
|
597
|
+
const right = [];
|
|
598
|
+
if (activity && phase.phase !== 'idle') {
|
|
599
|
+
const parts = activity.tools > 0 ? [glyphs.tools(activity.tools)] : [];
|
|
600
|
+
if (phase.phase === 'running' && activity.agents > 0) parts.push(glyphs.agents(activity.agents));
|
|
601
|
+
if (parts.length > 0) right.push(segment(parts.join(` ${glyphs.dot} `), (text) => paint.fg(text, 'dim'), { drop: 4 }));
|
|
492
602
|
}
|
|
493
|
-
if (
|
|
603
|
+
if (warning) right.push(segment(`${glyphs.warn} ${warning[1]}`, (text) => paint.fg(text, 'yellow', true), { drop: 1 }));
|
|
604
|
+
if (phase.phase === 'running') right.push(segment(`${glyphs.run} ${phase.timer ? `${glyphs.timer}${phase.timer}` : 'running'}`, (text) => paint.fg(text, 'accent', true), { drop: 2 }));
|
|
605
|
+
else if (phase.phase === 'complete') right.push(segment(`${glyphs.tick} complete ${phase.timer}`, (text) => paint.fg(text, 'green', true), { drop: 2 }));
|
|
606
|
+
else if (!warning) right.push(segment('idle', (text) => paint.fg(text, 'dim'), { drop: 2 }));
|
|
607
|
+
return layoutLine({ left, right, separator: { plain: ' ', painted: ' ' }, rail, maxCells, ellipsis: glyphs.ellipsis });
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// The detail line: checked plan tasks with the next one, the last test verdict and the long form of a
|
|
611
|
+
// warning on the left; the tool in flight with its clock on the right.
|
|
612
|
+
function detailLine({ workflow, live, warning, glyphs, paint, now, maxCells, rail }) {
|
|
613
|
+
const left = [];
|
|
614
|
+
const plan = workflow?.plan;
|
|
615
|
+
if (plan?.total > 0) {
|
|
616
|
+
const cells = Math.min(plan.total, 10);
|
|
617
|
+
const filled = Math.round((plan.done / plan.total) * cells);
|
|
618
|
+
const bar = `${glyphs.full.repeat(filled)}${glyphs.empty.repeat(cells - filled)}`;
|
|
619
|
+
left.push(segment(`task ${plan.done}/${plan.total} ${bar}`, () => `${paint.fg('task ', 'dim')}${paint.fg(`${plan.done}/${plan.total}`, 'accent', true)} ${paint.fg(glyphs.full.repeat(filled), 'accent')}${paint.fg(glyphs.empty.repeat(cells - filled), 'empty')}`));
|
|
620
|
+
if (typeof plan.next === 'string' && plan.next && plan.done < plan.total) {
|
|
621
|
+
const lead = `next ${glyphs.next} `;
|
|
622
|
+
const title = plainText(plan.next, '');
|
|
623
|
+
left.push(segment(`${lead}${title}`, (text) => `${paint.fg(lead, 'dim')}${paint.fg(text.slice(lead.length), 'accent')}`, {
|
|
624
|
+
drop: 5, clip: 6, minCells: width(lead) + 8, shrink: (cells) => `${lead}${clipCells(title, cells - width(lead), glyphs.ellipsis)}`,
|
|
625
|
+
}));
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
if (workflow?.test) {
|
|
494
629
|
const pass = workflow.test.verdict === 'pass';
|
|
495
|
-
|
|
496
|
-
parts.push({ plain: text, painted: paint(text, pass ? TEXT.green : RAIL.red) });
|
|
630
|
+
left.push(segment(`tests ${pass ? glyphs.tick : glyphs.cross}${workflow.test.count === undefined ? '' : ` ${workflow.test.count}`}`, (text) => paint.fg(text, pass ? 'green' : 'red', true), { drop: 3 }));
|
|
497
631
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
//
|
|
503
|
-
|
|
504
|
-
const tiers = [
|
|
505
|
-
{ rail: 4, live: 'full', tests: true },
|
|
506
|
-
{ rail: 2, live: 'full', tests: true },
|
|
507
|
-
{ rail: 2, live: 'full', tests: false },
|
|
508
|
-
{ rail: 2, live: 'short', tests: false },
|
|
509
|
-
{ rail: 1, live: false, tests: false },
|
|
510
|
-
{ rail: 1, compact: true, live: false, tests: false },
|
|
511
|
-
];
|
|
512
|
-
for (const tier of tiers) {
|
|
513
|
-
const row = renderWorkflow(workflow, live, running, depth, now, tier);
|
|
514
|
-
if (width(row.plain) <= maxCells && row.painted.length <= MAX_LINE_CHARACTERS) return row.painted || null;
|
|
632
|
+
if (warning) left.push(segment(`${glyphs.warn} ${warning[0]}`, (text) => paint.fg(text, 'yellow', true), { drop: 2 }));
|
|
633
|
+
const right = [];
|
|
634
|
+
if (live) {
|
|
635
|
+
const spinner = glyphs.spinner[Math.floor(now / 100) % glyphs.spinner.length];
|
|
636
|
+
// Alone on the line, the tool in flight sits by the elbow rather than across the row.
|
|
637
|
+
(left.length > 0 ? right : left).push(segment(`${spinner} ${toolLabel(live.name)} ${clock(now - live.startedAt)}`, (text) => paint.fg(text, 'accent', true), { drop: 1 }));
|
|
515
638
|
}
|
|
516
|
-
if (
|
|
517
|
-
const
|
|
518
|
-
return
|
|
639
|
+
if (left.length === 0 && right.length === 0) return null;
|
|
640
|
+
const elbow = { plain: `${rail.plain}${glyphs.elbow} `, painted: `${rail.painted}${paint.fg(`${glyphs.elbow} `, 'dim')}` };
|
|
641
|
+
return layoutLine({ left, right, separator: { plain: ` ${glyphs.dot} `, painted: paint.fg(` ${glyphs.dot} `, 'dim') }, rail: elbow, maxCells, ellipsis: glyphs.ellipsis });
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function figuresLine({ status, glyphs, paint, handoff, maxCells, rail }) {
|
|
645
|
+
return layoutLine({
|
|
646
|
+
left: figureLeads(figureSegments(status, { glyphs, paint, handoff }), glyphs, paint, false),
|
|
647
|
+
separator: { plain: ' ', painted: ' ' },
|
|
648
|
+
rail,
|
|
649
|
+
maxCells,
|
|
650
|
+
ellipsis: glyphs.ellipsis,
|
|
651
|
+
});
|
|
519
652
|
}
|
|
520
653
|
|
|
521
|
-
|
|
522
|
-
try {
|
|
523
|
-
|
|
524
|
-
} catch {
|
|
525
|
-
|
|
526
|
-
}
|
|
527
|
-
const cwd = typeof input?.cwd === 'string' ? input.cwd : '';
|
|
528
|
-
let record = null;
|
|
529
|
-
try {
|
|
530
|
-
|
|
531
|
-
} catch {
|
|
532
|
-
|
|
533
|
-
}
|
|
534
|
-
const discipline = record?.discipline ?? null;
|
|
535
|
-
const
|
|
536
|
-
const
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
const
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
if (process.env.LITGROK_HUD_LINES !== '1') {
|
|
654
|
+
function render(input) {
|
|
655
|
+
try {
|
|
656
|
+
recordContextForHandoff(input);
|
|
657
|
+
} catch {
|
|
658
|
+
// The row must always render; a failed record only means this refresh adds nothing for the handoff.
|
|
659
|
+
}
|
|
660
|
+
const cwd = typeof input?.cwd === 'string' ? input.cwd : '';
|
|
661
|
+
let record = null;
|
|
662
|
+
try {
|
|
663
|
+
record = readHudRecordForCwd(cwd);
|
|
664
|
+
} catch {
|
|
665
|
+
record = null;
|
|
666
|
+
}
|
|
667
|
+
const discipline = record?.discipline ?? null;
|
|
668
|
+
const colorSetting = process.env.LITGROK_HUD_COLOR;
|
|
669
|
+
const depth = colorSetting === '0' || Object.hasOwn(process.env, 'NO_COLOR') ? 0 : colorSetting === '256' ? 256 : colorSetting === '16' ? 16 : 24;
|
|
670
|
+
const paint = painter(depth);
|
|
671
|
+
const glyphSetting = process.env.LITGROK_HUD_GLYPHS;
|
|
672
|
+
const glyphs = Object.hasOwn(GLYPHS, glyphSetting ?? '') ? GLYPHS[glyphSetting] : GLYPHS.basic;
|
|
673
|
+
const nerd = glyphSetting === 'nerd';
|
|
674
|
+
// Grok sets COLUMNS to the width of the row; without it the row keeps to 79 cells.
|
|
675
|
+
const columns = Number.parseInt(process.env.COLUMNS ?? '', 10);
|
|
676
|
+
const maxCells = Number.isFinite(columns) && columns >= 20 ? Math.min(columns, 1000) - 1 : 79;
|
|
677
|
+
let handoff = { active: false, percent: null };
|
|
678
|
+
try {
|
|
679
|
+
handoff = resolveAutoHandoff({ env: process.env, dirs: statusDirs(input) });
|
|
680
|
+
} catch {
|
|
681
|
+
handoff = { active: false, percent: null };
|
|
682
|
+
}
|
|
683
|
+
const version = installedVersion();
|
|
684
|
+
const shared = { status: input, discipline, version, glyphs, paint, nerd, handoff, maxCells };
|
|
685
|
+
if (process.env.LITGROK_HUD_LINES === '1') return [compactRow(shared)];
|
|
686
|
+
|
|
555
687
|
const now = Date.now();
|
|
556
688
|
let activity = null;
|
|
557
689
|
try {
|
|
@@ -559,22 +691,35 @@ if (process.env.LITGROK_HUD_LINES !== '1') {
|
|
|
559
691
|
} catch {
|
|
560
692
|
activity = null;
|
|
561
693
|
}
|
|
562
|
-
|
|
563
|
-
const { running } = turn;
|
|
564
|
-
if (discipline) {
|
|
565
|
-
try {
|
|
566
|
-
const state = bandState(input, now, activity, turn);
|
|
567
|
-
if (state) band = formatBand(discipline, state, depth, process.env.LITGROK_HUD_GLYPHS === 'nerd', maxCells);
|
|
568
|
-
} catch {
|
|
569
|
-
band = null;
|
|
570
|
-
}
|
|
571
|
-
}
|
|
694
|
+
let workflow = null;
|
|
572
695
|
try {
|
|
573
|
-
|
|
574
|
-
const live = running ? activity?.live ?? null : null;
|
|
575
|
-
if (workflow?.stage || (discipline && (live || workflow?.test))) workflowLine = formatWorkflow(workflow, live, running, depth, now, maxCells);
|
|
696
|
+
workflow = readWorkflow(input);
|
|
576
697
|
} catch {
|
|
577
|
-
|
|
698
|
+
workflow = null;
|
|
578
699
|
}
|
|
700
|
+
// The card shows while a discipline is active or the session has a workflow stage; otherwise the
|
|
701
|
+
// HUD is the single row.
|
|
702
|
+
if (!discipline && !workflow?.stage) return [compactRow(shared)];
|
|
703
|
+
const turn = turnState(input, activity, now);
|
|
704
|
+
const phase = phaseOf(now, activity, turn);
|
|
705
|
+
const warning = warningText(input, activity, handoff);
|
|
706
|
+
const live = turn.running ? activity?.live ?? null : null;
|
|
707
|
+
const rail = railFor(paint, glyphs);
|
|
708
|
+
const lines = [titleLine({ ...shared, phase, warning, activity: phase.phase === 'idle' ? null : activity, rail })];
|
|
709
|
+
if (workflow?.stage) lines.push(mapLine(workflow, { paint, glyphs, nerd, running: turn.running, now }, rail, maxCells));
|
|
710
|
+
const detail = detailLine({ workflow, live, warning, glyphs, paint, now, maxCells, rail });
|
|
711
|
+
if (detail) lines.push(detail);
|
|
712
|
+
lines.push(figuresLine({ ...shared, rail }));
|
|
713
|
+
return lines;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
const input = await readInput();
|
|
718
|
+
let lines;
|
|
719
|
+
try {
|
|
720
|
+
lines = render(input);
|
|
721
|
+
} catch {
|
|
722
|
+
// Never a blank row and never a stack trace: the brand alone says the HUD is alive.
|
|
723
|
+
lines = [`[🔥LIT${installedVersion() ? ` v${installedVersion()}` : ''}] | ${plainText(input?.model?.display_name, 'grok')}`];
|
|
579
724
|
}
|
|
580
|
-
process.stdout.write(`${
|
|
725
|
+
process.stdout.write(`${lines.join('\n')}\n`);
|