specsmd 0.0.0-dev.86 → 0.0.0-dev.87
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/README.md +15 -0
- package/bin/cli.js +15 -1
- package/flows/fire/agents/builder/agent.md +2 -2
- package/flows/fire/agents/builder/skills/code-review/SKILL.md +1 -1
- package/flows/fire/agents/builder/skills/run-execute/SKILL.md +16 -7
- package/flows/fire/agents/builder/skills/run-execute/scripts/complete-run.cjs +22 -3
- package/flows/fire/agents/builder/skills/run-execute/scripts/init-run.cjs +63 -20
- package/flows/fire/agents/builder/skills/run-execute/scripts/update-checkpoint.cjs +254 -0
- package/flows/fire/agents/builder/skills/run-execute/scripts/update-phase.cjs +17 -6
- package/flows/fire/agents/builder/skills/run-status/SKILL.md +1 -1
- package/flows/fire/agents/orchestrator/agent.md +1 -1
- package/flows/fire/agents/orchestrator/skills/status/SKILL.md +2 -2
- package/flows/fire/memory-bank.yaml +4 -4
- package/lib/dashboard/aidlc/parser.js +581 -0
- package/lib/dashboard/fire/model.js +382 -0
- package/lib/dashboard/fire/parser.js +470 -0
- package/lib/dashboard/flow-detect.js +86 -0
- package/lib/dashboard/git/changes.js +362 -0
- package/lib/dashboard/git/worktrees.js +248 -0
- package/lib/dashboard/index.js +709 -0
- package/lib/dashboard/runtime/watch-runtime.js +122 -0
- package/lib/dashboard/simple/parser.js +293 -0
- package/lib/dashboard/tui/app.js +1675 -0
- package/lib/dashboard/tui/components/error-banner.js +35 -0
- package/lib/dashboard/tui/components/header.js +60 -0
- package/lib/dashboard/tui/components/help-footer.js +15 -0
- package/lib/dashboard/tui/components/stats-strip.js +35 -0
- package/lib/dashboard/tui/file-entries.js +383 -0
- package/lib/dashboard/tui/flow-builders.js +991 -0
- package/lib/dashboard/tui/git-builders.js +218 -0
- package/lib/dashboard/tui/helpers.js +236 -0
- package/lib/dashboard/tui/overlays.js +242 -0
- package/lib/dashboard/tui/preview.js +220 -0
- package/lib/dashboard/tui/renderer.js +76 -0
- package/lib/dashboard/tui/row-builders.js +797 -0
- package/lib/dashboard/tui/sections.js +45 -0
- package/lib/dashboard/tui/store.js +44 -0
- package/lib/dashboard/tui/views/overview-view.js +61 -0
- package/lib/dashboard/tui/views/runs-view.js +93 -0
- package/lib/dashboard/tui/worktree-builders.js +229 -0
- package/lib/installers/CodexInstaller.js +72 -1
- package/package.json +7 -3
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { spawnSync } = require('child_process');
|
|
3
|
+
const { fileExists } = require('./helpers');
|
|
4
|
+
|
|
5
|
+
function getGitChangesSnapshot(snapshot) {
|
|
6
|
+
const gitChanges = snapshot?.gitChanges;
|
|
7
|
+
if (!gitChanges || typeof gitChanges !== 'object') {
|
|
8
|
+
return {
|
|
9
|
+
available: false,
|
|
10
|
+
branch: '(unavailable)',
|
|
11
|
+
upstream: null,
|
|
12
|
+
ahead: 0,
|
|
13
|
+
behind: 0,
|
|
14
|
+
counts: {
|
|
15
|
+
total: 0,
|
|
16
|
+
staged: 0,
|
|
17
|
+
unstaged: 0,
|
|
18
|
+
untracked: 0,
|
|
19
|
+
conflicted: 0
|
|
20
|
+
},
|
|
21
|
+
staged: [],
|
|
22
|
+
unstaged: [],
|
|
23
|
+
untracked: [],
|
|
24
|
+
conflicted: [],
|
|
25
|
+
clean: true
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
...gitChanges,
|
|
30
|
+
counts: gitChanges.counts || {
|
|
31
|
+
total: 0,
|
|
32
|
+
staged: 0,
|
|
33
|
+
unstaged: 0,
|
|
34
|
+
untracked: 0,
|
|
35
|
+
conflicted: 0
|
|
36
|
+
},
|
|
37
|
+
staged: Array.isArray(gitChanges.staged) ? gitChanges.staged : [],
|
|
38
|
+
unstaged: Array.isArray(gitChanges.unstaged) ? gitChanges.unstaged : [],
|
|
39
|
+
untracked: Array.isArray(gitChanges.untracked) ? gitChanges.untracked : [],
|
|
40
|
+
conflicted: Array.isArray(gitChanges.conflicted) ? gitChanges.conflicted : []
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function readGitCommandLines(repoRoot, args, options = {}) {
|
|
45
|
+
if (typeof repoRoot !== 'string' || repoRoot.trim() === '' || !Array.isArray(args) || args.length === 0) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const acceptedStatuses = Array.isArray(options.acceptedStatuses) && options.acceptedStatuses.length > 0
|
|
50
|
+
? options.acceptedStatuses
|
|
51
|
+
: [0];
|
|
52
|
+
|
|
53
|
+
const result = spawnSync('git', args, {
|
|
54
|
+
cwd: repoRoot,
|
|
55
|
+
encoding: 'utf8',
|
|
56
|
+
maxBuffer: 8 * 1024 * 1024
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (result.error) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (typeof result.status === 'number' && !acceptedStatuses.includes(result.status)) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const lines = String(result.stdout || '')
|
|
68
|
+
.split(/\r?\n/)
|
|
69
|
+
.map((line) => line.trim())
|
|
70
|
+
.filter(Boolean);
|
|
71
|
+
|
|
72
|
+
const limit = Number.isFinite(options.limit) ? Math.max(1, Math.floor(options.limit)) : null;
|
|
73
|
+
if (limit == null || lines.length <= limit) {
|
|
74
|
+
return lines;
|
|
75
|
+
}
|
|
76
|
+
return lines.slice(0, limit);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function buildGitStatusPanelLines(snapshot) {
|
|
80
|
+
const git = getGitChangesSnapshot(snapshot);
|
|
81
|
+
if (!git.available) {
|
|
82
|
+
return [{
|
|
83
|
+
text: 'Repository unavailable in selected worktree',
|
|
84
|
+
color: 'red',
|
|
85
|
+
bold: true
|
|
86
|
+
}];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const tracking = git.upstream
|
|
90
|
+
? `${git.upstream} (${git.ahead > 0 ? `ahead ${git.ahead}` : 'ahead 0'}, ${git.behind > 0 ? `behind ${git.behind}` : 'behind 0'})`
|
|
91
|
+
: 'no upstream';
|
|
92
|
+
|
|
93
|
+
return [
|
|
94
|
+
{
|
|
95
|
+
text: `branch: ${git.branch}${git.detached ? ' [detached]' : ''}`,
|
|
96
|
+
color: 'green',
|
|
97
|
+
bold: true
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
text: `tracking: ${tracking}`,
|
|
101
|
+
color: 'gray',
|
|
102
|
+
bold: false
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
text: `changes: ${git.counts.total || 0} total`,
|
|
106
|
+
color: 'gray',
|
|
107
|
+
bold: false
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
text: `staged ${git.counts.staged || 0} | unstaged ${git.counts.unstaged || 0}`,
|
|
111
|
+
color: 'yellow',
|
|
112
|
+
bold: false
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
text: `untracked ${git.counts.untracked || 0} | conflicts ${git.counts.conflicted || 0}`,
|
|
116
|
+
color: 'yellow',
|
|
117
|
+
bold: false
|
|
118
|
+
}
|
|
119
|
+
];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function buildGitCommitRows(snapshot) {
|
|
123
|
+
const git = getGitChangesSnapshot(snapshot);
|
|
124
|
+
if (!git.available) {
|
|
125
|
+
return [{
|
|
126
|
+
kind: 'info',
|
|
127
|
+
key: 'git:commits:unavailable',
|
|
128
|
+
label: 'No commit history (git unavailable)',
|
|
129
|
+
selectable: false
|
|
130
|
+
}];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const commitLines = readGitCommandLines(git.rootPath, [
|
|
134
|
+
'-c',
|
|
135
|
+
'color.ui=false',
|
|
136
|
+
'log',
|
|
137
|
+
'--date=relative',
|
|
138
|
+
'--pretty=format:%h %s',
|
|
139
|
+
'--max-count=30'
|
|
140
|
+
], { limit: 30 });
|
|
141
|
+
|
|
142
|
+
if (commitLines.length === 0) {
|
|
143
|
+
return [{
|
|
144
|
+
kind: 'info',
|
|
145
|
+
key: 'git:commits:empty',
|
|
146
|
+
label: 'No commits found',
|
|
147
|
+
selectable: false
|
|
148
|
+
}];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return commitLines.map((line, index) => {
|
|
152
|
+
const firstSpace = line.indexOf(' ');
|
|
153
|
+
const commitHash = firstSpace > 0 ? line.slice(0, firstSpace) : '';
|
|
154
|
+
const message = firstSpace > 0 ? line.slice(firstSpace + 1) : line;
|
|
155
|
+
const label = commitHash ? `${commitHash} ${message}` : message;
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
kind: 'git-commit',
|
|
159
|
+
key: `git:commit:${commitHash || index}:${index}`,
|
|
160
|
+
label,
|
|
161
|
+
commitHash,
|
|
162
|
+
repoRoot: git.rootPath,
|
|
163
|
+
previewType: 'git-commit-diff',
|
|
164
|
+
selectable: true
|
|
165
|
+
};
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function buildGitChangeGroups(snapshot) {
|
|
170
|
+
const git = getGitChangesSnapshot(snapshot);
|
|
171
|
+
|
|
172
|
+
if (!git.available) {
|
|
173
|
+
return [];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const makeFiles = (items, scope) => items.map((item) => ({
|
|
177
|
+
label: item.relativePath,
|
|
178
|
+
path: item.path || path.join(git.rootPath || snapshot?.workspacePath || '', item.relativePath || ''),
|
|
179
|
+
scope,
|
|
180
|
+
allowMissing: true,
|
|
181
|
+
previewType: 'git-diff',
|
|
182
|
+
repoRoot: git.rootPath || snapshot?.workspacePath || '',
|
|
183
|
+
relativePath: item.relativePath || '',
|
|
184
|
+
bucket: item.bucket || scope
|
|
185
|
+
}));
|
|
186
|
+
|
|
187
|
+
const groups = [];
|
|
188
|
+
groups.push({
|
|
189
|
+
key: 'git:staged',
|
|
190
|
+
label: `staged (${git.counts.staged || 0})`,
|
|
191
|
+
files: makeFiles(git.staged, 'staged')
|
|
192
|
+
});
|
|
193
|
+
groups.push({
|
|
194
|
+
key: 'git:unstaged',
|
|
195
|
+
label: `unstaged (${git.counts.unstaged || 0})`,
|
|
196
|
+
files: makeFiles(git.unstaged, 'unstaged')
|
|
197
|
+
});
|
|
198
|
+
groups.push({
|
|
199
|
+
key: 'git:untracked',
|
|
200
|
+
label: `untracked (${git.counts.untracked || 0})`,
|
|
201
|
+
files: makeFiles(git.untracked, 'untracked')
|
|
202
|
+
});
|
|
203
|
+
groups.push({
|
|
204
|
+
key: 'git:conflicted',
|
|
205
|
+
label: `conflicts (${git.counts.conflicted || 0})`,
|
|
206
|
+
files: makeFiles(git.conflicted, 'conflicted')
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
return groups;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
module.exports = {
|
|
213
|
+
getGitChangesSnapshot,
|
|
214
|
+
readGitCommandLines,
|
|
215
|
+
buildGitStatusPanelLines,
|
|
216
|
+
buildGitCommitRows,
|
|
217
|
+
buildGitChangeGroups
|
|
218
|
+
};
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const stringWidthModule = require('string-width');
|
|
3
|
+
const sliceAnsiModule = require('slice-ansi');
|
|
4
|
+
|
|
5
|
+
const stringWidth = typeof stringWidthModule === 'function'
|
|
6
|
+
? stringWidthModule
|
|
7
|
+
: stringWidthModule.default;
|
|
8
|
+
const sliceAnsi = typeof sliceAnsiModule === 'function'
|
|
9
|
+
? sliceAnsiModule
|
|
10
|
+
: sliceAnsiModule.default;
|
|
11
|
+
|
|
12
|
+
function toDashboardError(error, defaultCode = 'DASHBOARD_ERROR') {
|
|
13
|
+
if (!error) {
|
|
14
|
+
return {
|
|
15
|
+
code: defaultCode,
|
|
16
|
+
message: 'Unknown dashboard error.'
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (typeof error === 'string') {
|
|
21
|
+
return {
|
|
22
|
+
code: defaultCode,
|
|
23
|
+
message: error
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (typeof error === 'object') {
|
|
28
|
+
return {
|
|
29
|
+
code: error.code || defaultCode,
|
|
30
|
+
message: error.message || 'Unknown dashboard error.',
|
|
31
|
+
details: error.details,
|
|
32
|
+
path: error.path,
|
|
33
|
+
hint: error.hint
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
code: defaultCode,
|
|
39
|
+
message: String(error)
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function safeJsonHash(value) {
|
|
44
|
+
try {
|
|
45
|
+
return JSON.stringify(value, (key, nestedValue) => {
|
|
46
|
+
if (key === 'generatedAt') {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
return nestedValue;
|
|
50
|
+
});
|
|
51
|
+
} catch {
|
|
52
|
+
return String(value);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function resolveIconSet() {
|
|
57
|
+
const mode = (process.env.SPECSMD_ICON_SET || 'auto').toLowerCase();
|
|
58
|
+
|
|
59
|
+
const ascii = {
|
|
60
|
+
runs: '[R]',
|
|
61
|
+
overview: '[O]',
|
|
62
|
+
health: '[H]',
|
|
63
|
+
git: '[G]',
|
|
64
|
+
runFile: '*',
|
|
65
|
+
activeFile: '>',
|
|
66
|
+
groupCollapsed: '>',
|
|
67
|
+
groupExpanded: 'v'
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const nerd = {
|
|
71
|
+
runs: '',
|
|
72
|
+
overview: '',
|
|
73
|
+
health: '',
|
|
74
|
+
git: '',
|
|
75
|
+
runFile: '',
|
|
76
|
+
activeFile: '',
|
|
77
|
+
groupCollapsed: '',
|
|
78
|
+
groupExpanded: ''
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
if (mode === 'ascii') {
|
|
82
|
+
return ascii;
|
|
83
|
+
}
|
|
84
|
+
if (mode === 'nerd') {
|
|
85
|
+
return nerd;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const locale = `${process.env.LC_ALL || ''}${process.env.LC_CTYPE || ''}${process.env.LANG || ''}`;
|
|
89
|
+
const isUtf8 = /utf-?8/i.test(locale);
|
|
90
|
+
const looksLikeVsCodeTerminal = (process.env.TERM_PROGRAM || '').toLowerCase().includes('vscode');
|
|
91
|
+
|
|
92
|
+
return isUtf8 && looksLikeVsCodeTerminal ? nerd : ascii;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function truncate(value, width) {
|
|
96
|
+
const text = String(value ?? '');
|
|
97
|
+
if (!Number.isFinite(width)) {
|
|
98
|
+
return text;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const safeWidth = Math.max(0, Math.floor(width));
|
|
102
|
+
if (safeWidth === 0) {
|
|
103
|
+
return '';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (stringWidth(text) <= safeWidth) {
|
|
107
|
+
return text;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (safeWidth <= 3) {
|
|
111
|
+
return sliceAnsi(text, 0, safeWidth);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const ellipsis = '...';
|
|
115
|
+
const bodyWidth = Math.max(0, safeWidth - stringWidth(ellipsis));
|
|
116
|
+
return `${sliceAnsi(text, 0, bodyWidth)}${ellipsis}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function resolveFrameWidth(columns) {
|
|
120
|
+
const safeColumns = Number.isFinite(columns) ? Math.max(1, Math.floor(columns)) : 120;
|
|
121
|
+
return safeColumns > 24 ? safeColumns - 1 : safeColumns;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function normalizePanelLine(line) {
|
|
125
|
+
if (line && typeof line === 'object' && !Array.isArray(line)) {
|
|
126
|
+
return {
|
|
127
|
+
text: typeof line.text === 'string' ? line.text : String(line.text ?? ''),
|
|
128
|
+
color: line.color,
|
|
129
|
+
bold: Boolean(line.bold),
|
|
130
|
+
selected: Boolean(line.selected),
|
|
131
|
+
loading: Boolean(line.loading)
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
text: String(line ?? ''),
|
|
137
|
+
color: undefined,
|
|
138
|
+
bold: false,
|
|
139
|
+
selected: false,
|
|
140
|
+
loading: false
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function fitLines(lines, maxLines, width) {
|
|
145
|
+
const safeLines = (Array.isArray(lines) ? lines : []).map((line) => {
|
|
146
|
+
const normalized = normalizePanelLine(line);
|
|
147
|
+
return {
|
|
148
|
+
...normalized,
|
|
149
|
+
text: truncate(normalized.text, width)
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (safeLines.length <= maxLines) {
|
|
154
|
+
return safeLines;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const selectedIndex = safeLines.findIndex((line) => line.selected);
|
|
158
|
+
if (selectedIndex >= 0) {
|
|
159
|
+
const windowSize = Math.max(1, maxLines);
|
|
160
|
+
let start = selectedIndex - Math.floor(windowSize / 2);
|
|
161
|
+
start = Math.max(0, start);
|
|
162
|
+
start = Math.min(start, Math.max(0, safeLines.length - windowSize));
|
|
163
|
+
return safeLines.slice(start, start + windowSize);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const visible = safeLines.slice(0, Math.max(1, maxLines - 1));
|
|
167
|
+
visible.push({
|
|
168
|
+
text: truncate(`... +${safeLines.length - visible.length} more`, width),
|
|
169
|
+
color: 'gray',
|
|
170
|
+
bold: false
|
|
171
|
+
});
|
|
172
|
+
return visible;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function formatTime(value) {
|
|
176
|
+
if (!value) {
|
|
177
|
+
return 'n/a';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const date = new Date(value);
|
|
181
|
+
if (Number.isNaN(date.getTime())) {
|
|
182
|
+
return value;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return date.toLocaleTimeString();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function fileExists(filePath) {
|
|
189
|
+
try {
|
|
190
|
+
return fs.statSync(filePath).isFile();
|
|
191
|
+
} catch {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function readFileTextSafe(filePath) {
|
|
197
|
+
try {
|
|
198
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
199
|
+
} catch {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function normalizeToken(value) {
|
|
205
|
+
if (typeof value !== 'string') {
|
|
206
|
+
return '';
|
|
207
|
+
}
|
|
208
|
+
return value.toLowerCase().trim().replace(/[\s-]+/g, '_');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function clampIndex(value, length) {
|
|
212
|
+
if (!Number.isFinite(value)) {
|
|
213
|
+
return 0;
|
|
214
|
+
}
|
|
215
|
+
if (!Number.isFinite(length) || length <= 0) {
|
|
216
|
+
return 0;
|
|
217
|
+
}
|
|
218
|
+
return Math.max(0, Math.min(length - 1, Math.floor(value)));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
module.exports = {
|
|
222
|
+
stringWidth,
|
|
223
|
+
sliceAnsi,
|
|
224
|
+
toDashboardError,
|
|
225
|
+
safeJsonHash,
|
|
226
|
+
resolveIconSet,
|
|
227
|
+
truncate,
|
|
228
|
+
resolveFrameWidth,
|
|
229
|
+
normalizePanelLine,
|
|
230
|
+
fitLines,
|
|
231
|
+
formatTime,
|
|
232
|
+
fileExists,
|
|
233
|
+
readFileTextSafe,
|
|
234
|
+
normalizeToken,
|
|
235
|
+
clampIndex
|
|
236
|
+
};
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
const { truncate } = require('./helpers');
|
|
2
|
+
|
|
3
|
+
function buildQuickHelpText(view, options = {}) {
|
|
4
|
+
const {
|
|
5
|
+
flow = 'fire',
|
|
6
|
+
previewOpen = false,
|
|
7
|
+
availableFlowCount = 1,
|
|
8
|
+
hasWorktrees = false
|
|
9
|
+
} = options;
|
|
10
|
+
const isAidlc = String(flow || '').toLowerCase() === 'aidlc';
|
|
11
|
+
const isSimple = String(flow || '').toLowerCase() === 'simple';
|
|
12
|
+
const activeLabel = isAidlc ? 'active bolt' : (isSimple ? 'active spec' : 'active run');
|
|
13
|
+
|
|
14
|
+
const parts = ['1/2/3/4/5 tabs', 'g/G sections'];
|
|
15
|
+
|
|
16
|
+
if (view === 'runs' || view === 'intents' || view === 'completed' || view === 'health' || view === 'git') {
|
|
17
|
+
if (previewOpen) {
|
|
18
|
+
parts.push('tab pane', '↑/↓ nav/scroll', 'v/space close');
|
|
19
|
+
} else {
|
|
20
|
+
parts.push('↑/↓ navigate', 'enter expand', 'v/space preview');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (view === 'runs') {
|
|
24
|
+
if (hasWorktrees) {
|
|
25
|
+
parts.push('b worktrees', 'u others');
|
|
26
|
+
}
|
|
27
|
+
parts.push('a current', 'f files');
|
|
28
|
+
} else if (view === 'git') {
|
|
29
|
+
parts.push('6 status', '7 files', '8 commits', '- diff');
|
|
30
|
+
}
|
|
31
|
+
parts.push(`tab1 ${activeLabel}`);
|
|
32
|
+
|
|
33
|
+
if (availableFlowCount > 1) {
|
|
34
|
+
parts.push('[/] flow');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
parts.push('r refresh', '? shortcuts', 'q quit');
|
|
38
|
+
return parts.join(' | ');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function buildGitCommandStrip(view, options = {}) {
|
|
42
|
+
const {
|
|
43
|
+
hasWorktrees = false,
|
|
44
|
+
previewOpen = false
|
|
45
|
+
} = options;
|
|
46
|
+
|
|
47
|
+
const parts = [];
|
|
48
|
+
|
|
49
|
+
if (view === 'runs') {
|
|
50
|
+
if (hasWorktrees) {
|
|
51
|
+
parts.push('b worktrees');
|
|
52
|
+
}
|
|
53
|
+
parts.push('a current', 'f files', 'enter expand');
|
|
54
|
+
} else if (view === 'intents') {
|
|
55
|
+
parts.push('n next', 'x completed', 'enter expand');
|
|
56
|
+
} else if (view === 'completed') {
|
|
57
|
+
parts.push('c completed', 'enter expand');
|
|
58
|
+
} else if (view === 'health') {
|
|
59
|
+
parts.push('s standards', 't stats', 'w warnings');
|
|
60
|
+
} else if (view === 'git') {
|
|
61
|
+
parts.push('6 status', '7 files', '8 commits', '- diff', 'space preview');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (previewOpen) {
|
|
65
|
+
parts.push('tab pane', 'j/k scroll');
|
|
66
|
+
} else {
|
|
67
|
+
parts.push('v preview');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
parts.push('1-5 views', 'g/G panels', 'r refresh', '? help', 'q quit');
|
|
71
|
+
return parts.join(' | ');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function buildGitCommandLogLine(options = {}) {
|
|
75
|
+
const {
|
|
76
|
+
statusLine = '',
|
|
77
|
+
activeFlow = 'fire',
|
|
78
|
+
watchEnabled = true,
|
|
79
|
+
watchStatus = 'watching',
|
|
80
|
+
selectedWorktreeLabel = null
|
|
81
|
+
} = options;
|
|
82
|
+
|
|
83
|
+
if (typeof statusLine === 'string' && statusLine.trim() !== '') {
|
|
84
|
+
return `Command Log | ${statusLine}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const watchLabel = watchEnabled ? watchStatus : 'off';
|
|
88
|
+
const worktreeSegment = selectedWorktreeLabel ? ` | wt:${selectedWorktreeLabel}` : '';
|
|
89
|
+
return `Command Log | flow:${String(activeFlow || 'fire').toUpperCase()} | watch:${watchLabel}${worktreeSegment} | ready`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function buildHelpOverlayLines(options = {}) {
|
|
93
|
+
const {
|
|
94
|
+
view = 'runs',
|
|
95
|
+
flow = 'fire',
|
|
96
|
+
previewOpen = false,
|
|
97
|
+
paneFocus = 'main',
|
|
98
|
+
availableFlowCount = 1,
|
|
99
|
+
showErrorSection = false,
|
|
100
|
+
hasWorktrees = false
|
|
101
|
+
} = options;
|
|
102
|
+
const isAidlc = String(flow || '').toLowerCase() === 'aidlc';
|
|
103
|
+
const isSimple = String(flow || '').toLowerCase() === 'simple';
|
|
104
|
+
const itemLabel = isAidlc ? 'bolt' : (isSimple ? 'spec' : 'run');
|
|
105
|
+
const itemPlural = isAidlc ? 'bolts' : (isSimple ? 'specs' : 'runs');
|
|
106
|
+
|
|
107
|
+
const lines = [
|
|
108
|
+
{ text: 'Global', color: 'cyan', bold: true },
|
|
109
|
+
'q or Ctrl+C quit',
|
|
110
|
+
'r refresh snapshot',
|
|
111
|
+
`1 active ${itemLabel} | 2 intents | 3 completed ${itemPlural} | 4 standards/health | 5 git changes`,
|
|
112
|
+
'g next section | G previous section',
|
|
113
|
+
'h/? toggle this shortcuts overlay',
|
|
114
|
+
'esc close overlays (help/preview/fullscreen)',
|
|
115
|
+
{ text: '', color: undefined, bold: false },
|
|
116
|
+
{ text: 'Tab 1 Active', color: 'yellow', bold: true },
|
|
117
|
+
...(hasWorktrees ? ['b focus worktrees section', 'u focus other-worktrees section'] : []),
|
|
118
|
+
`a focus active ${itemLabel}`,
|
|
119
|
+
`f focus ${itemLabel} files`,
|
|
120
|
+
'up/down or j/k move selection',
|
|
121
|
+
'enter expand/collapse selected folder row',
|
|
122
|
+
'v or space preview selected file',
|
|
123
|
+
'v twice quickly opens fullscreen preview overlay',
|
|
124
|
+
'tab switch focus between main and preview panes',
|
|
125
|
+
'o open selected file in system default app'
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
if (previewOpen) {
|
|
129
|
+
lines.push(`preview is open (focus: ${paneFocus})`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (availableFlowCount > 1) {
|
|
133
|
+
lines.push('[/] (and m) switch flow');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
lines.push(
|
|
137
|
+
{ text: '', color: undefined, bold: false },
|
|
138
|
+
{ text: 'Tab 2 Intents', color: 'green', bold: true },
|
|
139
|
+
'i focus intents',
|
|
140
|
+
'n next intents | x completed intents',
|
|
141
|
+
'left/right toggles next/completed when intents is focused',
|
|
142
|
+
{ text: '', color: undefined, bold: false },
|
|
143
|
+
{ text: 'Tab 3 Completed', color: 'blue', bold: true },
|
|
144
|
+
'c focus completed items',
|
|
145
|
+
{ text: '', color: undefined, bold: false },
|
|
146
|
+
{ text: 'Tab 4 Standards/Health', color: 'magenta', bold: true },
|
|
147
|
+
`s standards | t stats | w warnings${showErrorSection ? ' | e errors' : ''}`,
|
|
148
|
+
{ text: '', color: undefined, bold: false },
|
|
149
|
+
{ text: 'Tab 5 Git Changes', color: 'yellow', bold: true },
|
|
150
|
+
'7 files: select changed files and preview per-file diffs',
|
|
151
|
+
'8 commits: select a commit to preview the full commit diff',
|
|
152
|
+
'6 status | 7 files | 8 commits | - diff',
|
|
153
|
+
{ text: '', color: undefined, bold: false },
|
|
154
|
+
{ text: `Current view: ${String(view || 'runs').toUpperCase()}`, color: 'gray', bold: false }
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
return lines;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function colorizeMarkdownLine(line, inCodeBlock) {
|
|
161
|
+
const text = String(line ?? '');
|
|
162
|
+
|
|
163
|
+
if (/^\s*```/.test(text)) {
|
|
164
|
+
return {
|
|
165
|
+
color: 'magenta',
|
|
166
|
+
bold: true,
|
|
167
|
+
togglesCodeBlock: true
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (/^\s{0,3}#{1,6}\s+/.test(text)) {
|
|
172
|
+
return {
|
|
173
|
+
color: 'cyan',
|
|
174
|
+
bold: true,
|
|
175
|
+
togglesCodeBlock: false
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (/^\s*[-*+]\s+\[[ xX]\]/.test(text) || /^\s*[-*+]\s+/.test(text) || /^\s*\d+\.\s+/.test(text)) {
|
|
180
|
+
return {
|
|
181
|
+
color: 'yellow',
|
|
182
|
+
bold: false,
|
|
183
|
+
togglesCodeBlock: false
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (/^\s*>\s+/.test(text)) {
|
|
188
|
+
return {
|
|
189
|
+
color: 'gray',
|
|
190
|
+
bold: false,
|
|
191
|
+
togglesCodeBlock: false
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (/^\s*---\s*$/.test(text)) {
|
|
196
|
+
return {
|
|
197
|
+
color: 'yellow',
|
|
198
|
+
bold: false,
|
|
199
|
+
togglesCodeBlock: false
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (inCodeBlock) {
|
|
204
|
+
return {
|
|
205
|
+
color: 'green',
|
|
206
|
+
bold: false,
|
|
207
|
+
togglesCodeBlock: false
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
color: undefined,
|
|
213
|
+
bold: false,
|
|
214
|
+
togglesCodeBlock: false
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function sanitizeRenderLine(value) {
|
|
219
|
+
const raw = String(value ?? '');
|
|
220
|
+
const withoutAnsi = raw
|
|
221
|
+
// OSC sequences (e.g. hyperlinks / title set): ESC ] ... BEL or ESC \
|
|
222
|
+
.replace(/\u001B\][^\u0007]*(?:\u0007|\u001B\\)/g, '')
|
|
223
|
+
// CSI sequences
|
|
224
|
+
.replace(/\u001B\[[0-?]*[ -/]*[@-~]/g, '')
|
|
225
|
+
// 7-bit C1 control sequences
|
|
226
|
+
.replace(/\u001B[@-Z\\-_]/g, '')
|
|
227
|
+
// Any remaining ESC bytes
|
|
228
|
+
.replace(/\u001B/g, '')
|
|
229
|
+
// Carriage return can reposition cursor to column 0 and corrupt frame painting
|
|
230
|
+
.replace(/\r/g, '');
|
|
231
|
+
|
|
232
|
+
return withoutAnsi.replace(/[\u0000-\u0008\u000B-\u001A\u001C-\u001F\u007F]/g, '');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
module.exports = {
|
|
236
|
+
buildQuickHelpText,
|
|
237
|
+
buildGitCommandStrip,
|
|
238
|
+
buildGitCommandLogLine,
|
|
239
|
+
buildHelpOverlayLines,
|
|
240
|
+
colorizeMarkdownLine,
|
|
241
|
+
sanitizeRenderLine
|
|
242
|
+
};
|