specsmd 0.1.31 → 0.1.33
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/lib/dashboard/tui/app.js +1065 -146
- package/package.json +1 -1
package/lib/dashboard/tui/app.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const { spawnSync } = require('child_process');
|
|
3
4
|
const { createWatchRuntime } = require('../runtime/watch-runtime');
|
|
4
|
-
const { createInitialUIState
|
|
5
|
+
const { createInitialUIState } = require('./store');
|
|
5
6
|
|
|
6
7
|
function toDashboardError(error, defaultCode = 'DASHBOARD_ERROR') {
|
|
7
8
|
if (!error) {
|
|
@@ -54,14 +55,20 @@ function resolveIconSet() {
|
|
|
54
55
|
runs: '[R]',
|
|
55
56
|
overview: '[O]',
|
|
56
57
|
health: '[H]',
|
|
57
|
-
runFile: '*'
|
|
58
|
+
runFile: '*',
|
|
59
|
+
activeFile: '>',
|
|
60
|
+
groupCollapsed: '>',
|
|
61
|
+
groupExpanded: 'v'
|
|
58
62
|
};
|
|
59
63
|
|
|
60
64
|
const nerd = {
|
|
61
65
|
runs: '',
|
|
62
66
|
overview: '',
|
|
63
67
|
health: '',
|
|
64
|
-
runFile: ''
|
|
68
|
+
runFile: '',
|
|
69
|
+
activeFile: '',
|
|
70
|
+
groupCollapsed: '',
|
|
71
|
+
groupExpanded: ''
|
|
65
72
|
};
|
|
66
73
|
|
|
67
74
|
if (mode === 'ascii') {
|
|
@@ -96,14 +103,16 @@ function normalizePanelLine(line) {
|
|
|
96
103
|
return {
|
|
97
104
|
text: typeof line.text === 'string' ? line.text : String(line.text ?? ''),
|
|
98
105
|
color: line.color,
|
|
99
|
-
bold: Boolean(line.bold)
|
|
106
|
+
bold: Boolean(line.bold),
|
|
107
|
+
selected: Boolean(line.selected)
|
|
100
108
|
};
|
|
101
109
|
}
|
|
102
110
|
|
|
103
111
|
return {
|
|
104
112
|
text: String(line ?? ''),
|
|
105
113
|
color: undefined,
|
|
106
|
-
bold: false
|
|
114
|
+
bold: false,
|
|
115
|
+
selected: false
|
|
107
116
|
};
|
|
108
117
|
}
|
|
109
118
|
|
|
@@ -120,6 +129,15 @@ function fitLines(lines, maxLines, width) {
|
|
|
120
129
|
return safeLines;
|
|
121
130
|
}
|
|
122
131
|
|
|
132
|
+
const selectedIndex = safeLines.findIndex((line) => line.selected);
|
|
133
|
+
if (selectedIndex >= 0) {
|
|
134
|
+
const windowSize = Math.max(1, maxLines);
|
|
135
|
+
let start = selectedIndex - Math.floor(windowSize / 2);
|
|
136
|
+
start = Math.max(0, start);
|
|
137
|
+
start = Math.min(start, Math.max(0, safeLines.length - windowSize));
|
|
138
|
+
return safeLines.slice(start, start + windowSize);
|
|
139
|
+
}
|
|
140
|
+
|
|
123
141
|
const visible = safeLines.slice(0, Math.max(1, maxLines - 1));
|
|
124
142
|
visible.push({
|
|
125
143
|
text: truncate(`... +${safeLines.length - visible.length} more`, width),
|
|
@@ -719,52 +737,232 @@ function getPanelTitles(flow, snapshot) {
|
|
|
719
737
|
};
|
|
720
738
|
}
|
|
721
739
|
|
|
740
|
+
function getSectionOrderForView(view) {
|
|
741
|
+
if (view === 'overview') {
|
|
742
|
+
return ['project', 'intent-status', 'standards'];
|
|
743
|
+
}
|
|
744
|
+
if (view === 'health') {
|
|
745
|
+
return ['stats', 'warnings', 'error-details'];
|
|
746
|
+
}
|
|
747
|
+
return ['current-run', 'run-files', 'pending', 'completed'];
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function cycleSection(view, currentSectionKey, direction = 1, availableSections = null) {
|
|
751
|
+
const order = Array.isArray(availableSections) && availableSections.length > 0
|
|
752
|
+
? availableSections
|
|
753
|
+
: getSectionOrderForView(view);
|
|
754
|
+
if (order.length === 0) {
|
|
755
|
+
return currentSectionKey;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const currentIndex = order.indexOf(currentSectionKey);
|
|
759
|
+
const safeIndex = currentIndex >= 0 ? currentIndex : 0;
|
|
760
|
+
const nextIndex = (safeIndex + direction + order.length) % order.length;
|
|
761
|
+
return order[nextIndex];
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function fileExists(filePath) {
|
|
765
|
+
try {
|
|
766
|
+
return fs.statSync(filePath).isFile();
|
|
767
|
+
} catch {
|
|
768
|
+
return false;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function listMarkdownFiles(dirPath) {
|
|
773
|
+
try {
|
|
774
|
+
return fs.readdirSync(dirPath, { withFileTypes: true })
|
|
775
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
|
|
776
|
+
.map((entry) => entry.name)
|
|
777
|
+
.sort((a, b) => a.localeCompare(b));
|
|
778
|
+
} catch {
|
|
779
|
+
return [];
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function pushFileEntry(entries, seenPaths, candidate) {
|
|
784
|
+
if (!candidate || typeof candidate.path !== 'string' || typeof candidate.label !== 'string') {
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if (!fileExists(candidate.path)) {
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
if (seenPaths.has(candidate.path)) {
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
seenPaths.add(candidate.path);
|
|
797
|
+
entries.push({
|
|
798
|
+
path: candidate.path,
|
|
799
|
+
label: candidate.label,
|
|
800
|
+
scope: candidate.scope || 'other'
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function collectFireRunFiles(run) {
|
|
805
|
+
if (!run || typeof run.folderPath !== 'string') {
|
|
806
|
+
return [];
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const names = ['run.md'];
|
|
810
|
+
if (run.hasPlan) names.push('plan.md');
|
|
811
|
+
if (run.hasTestReport) names.push('test-report.md');
|
|
812
|
+
if (run.hasWalkthrough) names.push('walkthrough.md');
|
|
813
|
+
|
|
814
|
+
return names.map((fileName) => ({
|
|
815
|
+
label: `${run.id}/${fileName}`,
|
|
816
|
+
path: path.join(run.folderPath, fileName)
|
|
817
|
+
}));
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function collectAidlcBoltFiles(bolt) {
|
|
821
|
+
if (!bolt || typeof bolt.path !== 'string') {
|
|
822
|
+
return [];
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
const fileNames = Array.isArray(bolt.files) && bolt.files.length > 0
|
|
826
|
+
? bolt.files
|
|
827
|
+
: listMarkdownFiles(bolt.path);
|
|
828
|
+
|
|
829
|
+
return fileNames.map((fileName) => ({
|
|
830
|
+
label: `${bolt.id}/${fileName}`,
|
|
831
|
+
path: path.join(bolt.path, fileName)
|
|
832
|
+
}));
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function collectSimpleSpecFiles(spec) {
|
|
836
|
+
if (!spec || typeof spec.path !== 'string') {
|
|
837
|
+
return [];
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
const names = [];
|
|
841
|
+
if (spec.hasRequirements) names.push('requirements.md');
|
|
842
|
+
if (spec.hasDesign) names.push('design.md');
|
|
843
|
+
if (spec.hasTasks) names.push('tasks.md');
|
|
844
|
+
|
|
845
|
+
return names.map((fileName) => ({
|
|
846
|
+
label: `${spec.name}/${fileName}`,
|
|
847
|
+
path: path.join(spec.path, fileName)
|
|
848
|
+
}));
|
|
849
|
+
}
|
|
850
|
+
|
|
722
851
|
function getRunFileEntries(snapshot, flow) {
|
|
723
852
|
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
853
|
+
const entries = [];
|
|
854
|
+
const seenPaths = new Set();
|
|
724
855
|
|
|
725
856
|
if (effectiveFlow === 'aidlc') {
|
|
726
857
|
const bolt = getCurrentBolt(snapshot);
|
|
727
|
-
|
|
728
|
-
|
|
858
|
+
for (const file of collectAidlcBoltFiles(bolt)) {
|
|
859
|
+
pushFileEntry(entries, seenPaths, { ...file, scope: 'active' });
|
|
729
860
|
}
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
861
|
+
|
|
862
|
+
const pendingBolts = Array.isArray(snapshot?.pendingBolts) ? snapshot.pendingBolts : [];
|
|
863
|
+
for (const pendingBolt of pendingBolts) {
|
|
864
|
+
for (const file of collectAidlcBoltFiles(pendingBolt)) {
|
|
865
|
+
pushFileEntry(entries, seenPaths, { ...file, scope: 'upcoming' });
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
const completedBolts = Array.isArray(snapshot?.completedBolts) ? snapshot.completedBolts : [];
|
|
870
|
+
for (const completedBolt of completedBolts) {
|
|
871
|
+
for (const file of collectAidlcBoltFiles(completedBolt)) {
|
|
872
|
+
pushFileEntry(entries, seenPaths, { ...file, scope: 'completed' });
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
const intentIds = new Set([
|
|
877
|
+
...pendingBolts.map((item) => item?.intent).filter(Boolean),
|
|
878
|
+
...completedBolts.map((item) => item?.intent).filter(Boolean)
|
|
879
|
+
]);
|
|
880
|
+
|
|
881
|
+
for (const intentId of intentIds) {
|
|
882
|
+
const intentPath = path.join(snapshot?.rootPath || '', 'intents', intentId);
|
|
883
|
+
pushFileEntry(entries, seenPaths, {
|
|
884
|
+
label: `${intentId}/requirements.md`,
|
|
885
|
+
path: path.join(intentPath, 'requirements.md'),
|
|
886
|
+
scope: 'intent'
|
|
887
|
+
});
|
|
888
|
+
pushFileEntry(entries, seenPaths, {
|
|
889
|
+
label: `${intentId}/system-context.md`,
|
|
890
|
+
path: path.join(intentPath, 'system-context.md'),
|
|
891
|
+
scope: 'intent'
|
|
892
|
+
});
|
|
893
|
+
pushFileEntry(entries, seenPaths, {
|
|
894
|
+
label: `${intentId}/units.md`,
|
|
895
|
+
path: path.join(intentPath, 'units.md'),
|
|
896
|
+
scope: 'intent'
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
return entries;
|
|
735
900
|
}
|
|
736
901
|
|
|
737
902
|
if (effectiveFlow === 'simple') {
|
|
738
903
|
const spec = getCurrentSpec(snapshot);
|
|
739
|
-
|
|
740
|
-
|
|
904
|
+
for (const file of collectSimpleSpecFiles(spec)) {
|
|
905
|
+
pushFileEntry(entries, seenPaths, { ...file, scope: 'active' });
|
|
741
906
|
}
|
|
742
907
|
|
|
743
|
-
const
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
908
|
+
const pendingSpecs = Array.isArray(snapshot?.pendingSpecs) ? snapshot.pendingSpecs : [];
|
|
909
|
+
for (const pendingSpec of pendingSpecs) {
|
|
910
|
+
for (const file of collectSimpleSpecFiles(pendingSpec)) {
|
|
911
|
+
pushFileEntry(entries, seenPaths, { ...file, scope: 'upcoming' });
|
|
912
|
+
}
|
|
913
|
+
}
|
|
747
914
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
915
|
+
const completedSpecs = Array.isArray(snapshot?.completedSpecs) ? snapshot.completedSpecs : [];
|
|
916
|
+
for (const completedSpec of completedSpecs) {
|
|
917
|
+
for (const file of collectSimpleSpecFiles(completedSpec)) {
|
|
918
|
+
pushFileEntry(entries, seenPaths, { ...file, scope: 'completed' });
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
return entries;
|
|
752
923
|
}
|
|
753
924
|
|
|
754
925
|
const run = getCurrentRun(snapshot);
|
|
755
|
-
|
|
756
|
-
|
|
926
|
+
for (const file of collectFireRunFiles(run)) {
|
|
927
|
+
pushFileEntry(entries, seenPaths, { ...file, scope: 'active' });
|
|
757
928
|
}
|
|
758
929
|
|
|
759
|
-
const
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
930
|
+
const pendingItems = Array.isArray(snapshot?.pendingItems) ? snapshot.pendingItems : [];
|
|
931
|
+
for (const pendingItem of pendingItems) {
|
|
932
|
+
pushFileEntry(entries, seenPaths, {
|
|
933
|
+
label: `${pendingItem?.intentId || 'intent'}/${pendingItem?.id || 'work-item'}.md`,
|
|
934
|
+
path: pendingItem?.filePath,
|
|
935
|
+
scope: 'upcoming'
|
|
936
|
+
});
|
|
763
937
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
938
|
+
if (pendingItem?.intentId) {
|
|
939
|
+
pushFileEntry(entries, seenPaths, {
|
|
940
|
+
label: `${pendingItem.intentId}/brief.md`,
|
|
941
|
+
path: path.join(snapshot?.rootPath || '', 'intents', pendingItem.intentId, 'brief.md'),
|
|
942
|
+
scope: 'intent'
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
const completedRuns = Array.isArray(snapshot?.completedRuns) ? snapshot.completedRuns : [];
|
|
948
|
+
for (const completedRun of completedRuns) {
|
|
949
|
+
for (const file of collectFireRunFiles(completedRun)) {
|
|
950
|
+
pushFileEntry(entries, seenPaths, { ...file, scope: 'completed' });
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
const completedIntents = Array.isArray(snapshot?.intents)
|
|
955
|
+
? snapshot.intents.filter((intent) => intent?.status === 'completed')
|
|
956
|
+
: [];
|
|
957
|
+
for (const intent of completedIntents) {
|
|
958
|
+
pushFileEntry(entries, seenPaths, {
|
|
959
|
+
label: `${intent.id}/brief.md`,
|
|
960
|
+
path: path.join(snapshot?.rootPath || '', 'intents', intent.id, 'brief.md'),
|
|
961
|
+
scope: 'intent'
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
return entries;
|
|
768
966
|
}
|
|
769
967
|
|
|
770
968
|
function clampIndex(value, length) {
|
|
@@ -778,32 +976,372 @@ function clampIndex(value, length) {
|
|
|
778
976
|
}
|
|
779
977
|
|
|
780
978
|
function getNoFileMessage(flow) {
|
|
781
|
-
|
|
782
|
-
|
|
979
|
+
return `No selectable files for ${String(flow || 'flow').toUpperCase()}`;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function formatScope(scope) {
|
|
983
|
+
if (scope === 'active') return 'ACTIVE';
|
|
984
|
+
if (scope === 'upcoming') return 'UPNEXT';
|
|
985
|
+
if (scope === 'completed') return 'DONE';
|
|
986
|
+
if (scope === 'intent') return 'INTENT';
|
|
987
|
+
return 'FILE';
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function getNoPendingMessage(flow) {
|
|
991
|
+
if (flow === 'aidlc') return 'No queued bolts';
|
|
992
|
+
if (flow === 'simple') return 'No pending specs';
|
|
993
|
+
return 'No pending work items';
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
function getNoCompletedMessage(flow) {
|
|
997
|
+
if (flow === 'aidlc') return 'No completed bolts yet';
|
|
998
|
+
if (flow === 'simple') return 'No completed specs yet';
|
|
999
|
+
return 'No completed runs yet';
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function toRunFileRows(fileEntries, flow) {
|
|
1003
|
+
if (!Array.isArray(fileEntries) || fileEntries.length === 0) {
|
|
1004
|
+
return [{
|
|
1005
|
+
kind: 'info',
|
|
1006
|
+
key: 'run-files:empty',
|
|
1007
|
+
label: getNoFileMessage(flow),
|
|
1008
|
+
selectable: false
|
|
1009
|
+
}];
|
|
783
1010
|
}
|
|
784
|
-
|
|
785
|
-
|
|
1011
|
+
|
|
1012
|
+
return fileEntries.map((file, index) => ({
|
|
1013
|
+
kind: 'file',
|
|
1014
|
+
key: `run-files:${file.path}:${index}`,
|
|
1015
|
+
label: file.label,
|
|
1016
|
+
path: file.path,
|
|
1017
|
+
scope: file.scope || 'file',
|
|
1018
|
+
selectable: true
|
|
1019
|
+
}));
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
function collectAidlcIntentContextFiles(snapshot, intentId) {
|
|
1023
|
+
if (!snapshot || typeof intentId !== 'string' || intentId.trim() === '') {
|
|
1024
|
+
return [];
|
|
786
1025
|
}
|
|
787
|
-
|
|
1026
|
+
|
|
1027
|
+
const intentPath = path.join(snapshot.rootPath || '', 'intents', intentId);
|
|
1028
|
+
return [
|
|
1029
|
+
{
|
|
1030
|
+
label: `${intentId}/requirements.md`,
|
|
1031
|
+
path: path.join(intentPath, 'requirements.md'),
|
|
1032
|
+
scope: 'intent'
|
|
1033
|
+
},
|
|
1034
|
+
{
|
|
1035
|
+
label: `${intentId}/system-context.md`,
|
|
1036
|
+
path: path.join(intentPath, 'system-context.md'),
|
|
1037
|
+
scope: 'intent'
|
|
1038
|
+
},
|
|
1039
|
+
{
|
|
1040
|
+
label: `${intentId}/units.md`,
|
|
1041
|
+
path: path.join(intentPath, 'units.md'),
|
|
1042
|
+
scope: 'intent'
|
|
1043
|
+
}
|
|
1044
|
+
];
|
|
788
1045
|
}
|
|
789
1046
|
|
|
790
|
-
function
|
|
791
|
-
|
|
792
|
-
|
|
1047
|
+
function filterExistingFiles(files) {
|
|
1048
|
+
return (Array.isArray(files) ? files : []).filter((file) =>
|
|
1049
|
+
file && typeof file.path === 'string' && typeof file.label === 'string' && fileExists(file.path)
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function buildPendingGroups(snapshot, flow) {
|
|
1054
|
+
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
1055
|
+
|
|
1056
|
+
if (effectiveFlow === 'aidlc') {
|
|
1057
|
+
const pendingBolts = Array.isArray(snapshot?.pendingBolts) ? snapshot.pendingBolts : [];
|
|
1058
|
+
return pendingBolts.map((bolt, index) => {
|
|
1059
|
+
const deps = Array.isArray(bolt?.blockedBy) && bolt.blockedBy.length > 0
|
|
1060
|
+
? ` blocked_by:${bolt.blockedBy.join(',')}`
|
|
1061
|
+
: '';
|
|
1062
|
+
const location = `${bolt?.intent || 'unknown'}/${bolt?.unit || 'unknown'}`;
|
|
1063
|
+
const boltFiles = collectAidlcBoltFiles(bolt);
|
|
1064
|
+
const intentFiles = collectAidlcIntentContextFiles(snapshot, bolt?.intent);
|
|
1065
|
+
return {
|
|
1066
|
+
key: `pending:bolt:${bolt?.id || index}`,
|
|
1067
|
+
label: `${bolt?.id || 'unknown'} (${bolt?.status || 'pending'}) in ${location}${deps}`,
|
|
1068
|
+
files: filterExistingFiles([...boltFiles, ...intentFiles])
|
|
1069
|
+
};
|
|
1070
|
+
});
|
|
793
1071
|
}
|
|
794
1072
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
1073
|
+
if (effectiveFlow === 'simple') {
|
|
1074
|
+
const pendingSpecs = Array.isArray(snapshot?.pendingSpecs) ? snapshot.pendingSpecs : [];
|
|
1075
|
+
return pendingSpecs.map((spec, index) => ({
|
|
1076
|
+
key: `pending:spec:${spec?.name || index}`,
|
|
1077
|
+
label: `${spec?.name || 'unknown'} (${spec?.state || 'pending'}) ${spec?.tasksCompleted || 0}/${spec?.tasksTotal || 0} tasks`,
|
|
1078
|
+
files: filterExistingFiles(collectSimpleSpecFiles(spec))
|
|
1079
|
+
}));
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
const pendingItems = Array.isArray(snapshot?.pendingItems) ? snapshot.pendingItems : [];
|
|
1083
|
+
return pendingItems.map((item, index) => {
|
|
1084
|
+
const deps = Array.isArray(item?.dependencies) && item.dependencies.length > 0
|
|
1085
|
+
? ` deps:${item.dependencies.join(',')}`
|
|
1086
|
+
: '';
|
|
1087
|
+
const intentTitle = item?.intentTitle || item?.intentId || 'unknown-intent';
|
|
1088
|
+
const files = [];
|
|
1089
|
+
|
|
1090
|
+
if (item?.filePath) {
|
|
1091
|
+
files.push({
|
|
1092
|
+
label: `${item?.intentId || 'intent'}/${item?.id || 'work-item'}.md`,
|
|
1093
|
+
path: item.filePath,
|
|
1094
|
+
scope: 'upcoming'
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
if (item?.intentId) {
|
|
1098
|
+
files.push({
|
|
1099
|
+
label: `${item.intentId}/brief.md`,
|
|
1100
|
+
path: path.join(snapshot?.rootPath || '', 'intents', item.intentId, 'brief.md'),
|
|
1101
|
+
scope: 'intent'
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
|
|
799
1105
|
return {
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
1106
|
+
key: `pending:item:${item?.intentId || 'intent'}:${item?.id || index}`,
|
|
1107
|
+
label: `${item?.id || 'work-item'} (${item?.mode || 'confirm'}/${item?.complexity || 'medium'}) in ${intentTitle}${deps}`,
|
|
1108
|
+
files: filterExistingFiles(files)
|
|
803
1109
|
};
|
|
804
1110
|
});
|
|
805
1111
|
}
|
|
806
1112
|
|
|
1113
|
+
function buildCompletedGroups(snapshot, flow) {
|
|
1114
|
+
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
1115
|
+
|
|
1116
|
+
if (effectiveFlow === 'aidlc') {
|
|
1117
|
+
const completedBolts = Array.isArray(snapshot?.completedBolts) ? snapshot.completedBolts : [];
|
|
1118
|
+
return completedBolts.map((bolt, index) => {
|
|
1119
|
+
const boltFiles = collectAidlcBoltFiles(bolt);
|
|
1120
|
+
const intentFiles = collectAidlcIntentContextFiles(snapshot, bolt?.intent);
|
|
1121
|
+
return {
|
|
1122
|
+
key: `completed:bolt:${bolt?.id || index}`,
|
|
1123
|
+
label: `${bolt?.id || 'unknown'} [${bolt?.type || 'bolt'}] done at ${bolt?.completedAt || 'unknown'}`,
|
|
1124
|
+
files: filterExistingFiles([...boltFiles, ...intentFiles])
|
|
1125
|
+
};
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
if (effectiveFlow === 'simple') {
|
|
1130
|
+
const completedSpecs = Array.isArray(snapshot?.completedSpecs) ? snapshot.completedSpecs : [];
|
|
1131
|
+
return completedSpecs.map((spec, index) => ({
|
|
1132
|
+
key: `completed:spec:${spec?.name || index}`,
|
|
1133
|
+
label: `${spec?.name || 'unknown'} done at ${spec?.updatedAt || 'unknown'} (${spec?.tasksCompleted || 0}/${spec?.tasksTotal || 0})`,
|
|
1134
|
+
files: filterExistingFiles(collectSimpleSpecFiles(spec))
|
|
1135
|
+
}));
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
const groups = [];
|
|
1139
|
+
const completedRuns = Array.isArray(snapshot?.completedRuns) ? snapshot.completedRuns : [];
|
|
1140
|
+
for (let index = 0; index < completedRuns.length; index += 1) {
|
|
1141
|
+
const run = completedRuns[index];
|
|
1142
|
+
const workItems = Array.isArray(run?.workItems) ? run.workItems : [];
|
|
1143
|
+
const completed = workItems.filter((item) => item.status === 'completed').length;
|
|
1144
|
+
groups.push({
|
|
1145
|
+
key: `completed:run:${run?.id || index}`,
|
|
1146
|
+
label: `${run?.id || 'run'} [${run?.scope || 'single'}] ${completed}/${workItems.length} done at ${run?.completedAt || 'unknown'}`,
|
|
1147
|
+
files: filterExistingFiles(collectFireRunFiles(run).map((file) => ({ ...file, scope: 'completed' })))
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
const completedIntents = Array.isArray(snapshot?.intents)
|
|
1152
|
+
? snapshot.intents.filter((intent) => intent?.status === 'completed')
|
|
1153
|
+
: [];
|
|
1154
|
+
for (let index = 0; index < completedIntents.length; index += 1) {
|
|
1155
|
+
const intent = completedIntents[index];
|
|
1156
|
+
groups.push({
|
|
1157
|
+
key: `completed:intent:${intent?.id || index}`,
|
|
1158
|
+
label: `intent ${intent?.id || 'unknown'} [completed]`,
|
|
1159
|
+
files: filterExistingFiles([{
|
|
1160
|
+
label: `${intent?.id || 'intent'}/brief.md`,
|
|
1161
|
+
path: path.join(snapshot?.rootPath || '', 'intents', intent?.id || '', 'brief.md'),
|
|
1162
|
+
scope: 'intent'
|
|
1163
|
+
}])
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
return groups;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
function toExpandableRows(groups, emptyLabel, expandedGroups) {
|
|
1171
|
+
if (!Array.isArray(groups) || groups.length === 0) {
|
|
1172
|
+
return [{
|
|
1173
|
+
kind: 'info',
|
|
1174
|
+
key: 'section:empty',
|
|
1175
|
+
label: emptyLabel,
|
|
1176
|
+
selectable: false
|
|
1177
|
+
}];
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
const rows = [];
|
|
1181
|
+
|
|
1182
|
+
for (const group of groups) {
|
|
1183
|
+
const files = filterExistingFiles(group?.files);
|
|
1184
|
+
const expandable = files.length > 0;
|
|
1185
|
+
const expanded = expandable && Boolean(expandedGroups?.[group.key]);
|
|
1186
|
+
|
|
1187
|
+
rows.push({
|
|
1188
|
+
kind: 'group',
|
|
1189
|
+
key: group.key,
|
|
1190
|
+
label: group.label,
|
|
1191
|
+
expandable,
|
|
1192
|
+
expanded,
|
|
1193
|
+
selectable: true
|
|
1194
|
+
});
|
|
1195
|
+
|
|
1196
|
+
if (expanded) {
|
|
1197
|
+
for (let index = 0; index < files.length; index += 1) {
|
|
1198
|
+
const file = files[index];
|
|
1199
|
+
rows.push({
|
|
1200
|
+
kind: 'file',
|
|
1201
|
+
key: `${group.key}:file:${file.path}:${index}`,
|
|
1202
|
+
label: file.label,
|
|
1203
|
+
path: file.path,
|
|
1204
|
+
scope: file.scope || 'file',
|
|
1205
|
+
selectable: true
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
return rows;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
function buildInteractiveRowsLines(rows, selectedIndex, icons, width, isFocusedSection) {
|
|
1215
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
1216
|
+
return [{ text: '', color: undefined, bold: false, selected: false }];
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
const clampedIndex = clampIndex(selectedIndex, rows.length);
|
|
1220
|
+
|
|
1221
|
+
return rows.map((row, index) => {
|
|
1222
|
+
const selectable = row?.selectable !== false;
|
|
1223
|
+
const isSelected = selectable && index === clampedIndex;
|
|
1224
|
+
const cursor = isSelected
|
|
1225
|
+
? (isFocusedSection ? (icons.activeFile || '>') : '•')
|
|
1226
|
+
: ' ';
|
|
1227
|
+
|
|
1228
|
+
if (row.kind === 'group') {
|
|
1229
|
+
const marker = row.expandable
|
|
1230
|
+
? (row.expanded ? (icons.groupExpanded || 'v') : (icons.groupCollapsed || '>'))
|
|
1231
|
+
: '-';
|
|
1232
|
+
return {
|
|
1233
|
+
text: truncate(`${cursor} ${marker} ${row.label}`, width),
|
|
1234
|
+
color: isSelected ? (isFocusedSection ? 'green' : 'cyan') : undefined,
|
|
1235
|
+
bold: isSelected,
|
|
1236
|
+
selected: isSelected
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
if (row.kind === 'file') {
|
|
1241
|
+
const scope = row.scope ? `[${formatScope(row.scope)}] ` : '';
|
|
1242
|
+
return {
|
|
1243
|
+
text: truncate(`${cursor} ${icons.runFile} ${scope}${row.label}`, width),
|
|
1244
|
+
color: isSelected ? (isFocusedSection ? 'green' : 'cyan') : 'gray',
|
|
1245
|
+
bold: isSelected,
|
|
1246
|
+
selected: isSelected
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
return {
|
|
1251
|
+
text: truncate(` ${row.label || ''}`, width),
|
|
1252
|
+
color: 'gray',
|
|
1253
|
+
bold: false,
|
|
1254
|
+
selected: false
|
|
1255
|
+
};
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
function getSelectedRow(rows, selectedIndex) {
|
|
1260
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
1261
|
+
return null;
|
|
1262
|
+
}
|
|
1263
|
+
return rows[clampIndex(selectedIndex, rows.length)] || null;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
function rowToFileEntry(row) {
|
|
1267
|
+
if (!row || row.kind !== 'file' || typeof row.path !== 'string') {
|
|
1268
|
+
return null;
|
|
1269
|
+
}
|
|
1270
|
+
return {
|
|
1271
|
+
label: row.label || path.basename(row.path),
|
|
1272
|
+
path: row.path,
|
|
1273
|
+
scope: row.scope || 'file'
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
function moveRowSelection(rows, currentIndex, direction) {
|
|
1278
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
1279
|
+
return 0;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
const clamped = clampIndex(currentIndex, rows.length);
|
|
1283
|
+
const step = direction >= 0 ? 1 : -1;
|
|
1284
|
+
let next = clamped + step;
|
|
1285
|
+
|
|
1286
|
+
while (next >= 0 && next < rows.length) {
|
|
1287
|
+
if (rows[next]?.selectable !== false) {
|
|
1288
|
+
return next;
|
|
1289
|
+
}
|
|
1290
|
+
next += step;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
return clamped;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
function openFileWithDefaultApp(filePath) {
|
|
1297
|
+
if (typeof filePath !== 'string' || filePath.trim() === '') {
|
|
1298
|
+
return {
|
|
1299
|
+
ok: false,
|
|
1300
|
+
message: 'No file selected to open.'
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
if (!fileExists(filePath)) {
|
|
1305
|
+
return {
|
|
1306
|
+
ok: false,
|
|
1307
|
+
message: `File not found: ${filePath}`
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
let command = null;
|
|
1312
|
+
let args = [];
|
|
1313
|
+
|
|
1314
|
+
if (process.platform === 'darwin') {
|
|
1315
|
+
command = 'open';
|
|
1316
|
+
args = [filePath];
|
|
1317
|
+
} else if (process.platform === 'win32') {
|
|
1318
|
+
command = 'cmd';
|
|
1319
|
+
args = ['/c', 'start', '', filePath];
|
|
1320
|
+
} else {
|
|
1321
|
+
command = 'xdg-open';
|
|
1322
|
+
args = [filePath];
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
const result = spawnSync(command, args, { stdio: 'ignore' });
|
|
1326
|
+
if (result.error) {
|
|
1327
|
+
return {
|
|
1328
|
+
ok: false,
|
|
1329
|
+
message: `Unable to open file: ${result.error.message}`
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
if (typeof result.status === 'number' && result.status !== 0) {
|
|
1333
|
+
return {
|
|
1334
|
+
ok: false,
|
|
1335
|
+
message: `Open command failed with exit code ${result.status}.`
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
return {
|
|
1340
|
+
ok: true,
|
|
1341
|
+
message: `Opened ${filePath}`
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
|
|
807
1345
|
function colorizeMarkdownLine(line, inCodeBlock) {
|
|
808
1346
|
const text = String(line ?? '');
|
|
809
1347
|
|
|
@@ -862,7 +1400,9 @@ function colorizeMarkdownLine(line, inCodeBlock) {
|
|
|
862
1400
|
};
|
|
863
1401
|
}
|
|
864
1402
|
|
|
865
|
-
function buildPreviewLines(fileEntry, width, scrollOffset) {
|
|
1403
|
+
function buildPreviewLines(fileEntry, width, scrollOffset, options = {}) {
|
|
1404
|
+
const fullDocument = options?.fullDocument === true;
|
|
1405
|
+
|
|
866
1406
|
if (!fileEntry || typeof fileEntry.path !== 'string') {
|
|
867
1407
|
return [{ text: truncate('No file selected', width), color: 'gray', bold: false }];
|
|
868
1408
|
}
|
|
@@ -872,7 +1412,7 @@ function buildPreviewLines(fileEntry, width, scrollOffset) {
|
|
|
872
1412
|
content = fs.readFileSync(fileEntry.path, 'utf8');
|
|
873
1413
|
} catch (error) {
|
|
874
1414
|
return [{
|
|
875
|
-
text: truncate(`Unable to read ${fileEntry.
|
|
1415
|
+
text: truncate(`Unable to read ${fileEntry.label || fileEntry.path}: ${error.message}`, width),
|
|
876
1416
|
color: 'red',
|
|
877
1417
|
bold: false
|
|
878
1418
|
}];
|
|
@@ -885,8 +1425,8 @@ function buildPreviewLines(fileEntry, width, scrollOffset) {
|
|
|
885
1425
|
bold: true
|
|
886
1426
|
};
|
|
887
1427
|
|
|
888
|
-
const cappedLines = rawLines.slice(0, 300);
|
|
889
|
-
const hiddenLineCount = Math.max(0, rawLines.length - cappedLines.length);
|
|
1428
|
+
const cappedLines = fullDocument ? rawLines : rawLines.slice(0, 300);
|
|
1429
|
+
const hiddenLineCount = fullDocument ? 0 : Math.max(0, rawLines.length - cappedLines.length);
|
|
890
1430
|
let inCodeBlock = false;
|
|
891
1431
|
|
|
892
1432
|
const highlighted = cappedLines.map((rawLine, index) => {
|
|
@@ -977,23 +1517,31 @@ function createDashboardApp(deps) {
|
|
|
977
1517
|
maxLines,
|
|
978
1518
|
borderColor,
|
|
979
1519
|
marginBottom,
|
|
980
|
-
dense
|
|
1520
|
+
dense,
|
|
1521
|
+
focused
|
|
981
1522
|
} = props;
|
|
982
1523
|
|
|
983
1524
|
const contentWidth = Math.max(18, width - 4);
|
|
984
1525
|
const visibleLines = fitLines(lines, maxLines, contentWidth);
|
|
1526
|
+
const panelBorderColor = focused ? 'cyan' : (borderColor || 'gray');
|
|
1527
|
+
const titleColor = focused ? 'black' : 'cyan';
|
|
1528
|
+
const titleBackground = focused ? 'cyan' : undefined;
|
|
985
1529
|
|
|
986
1530
|
return React.createElement(
|
|
987
1531
|
Box,
|
|
988
1532
|
{
|
|
989
1533
|
flexDirection: 'column',
|
|
990
1534
|
borderStyle: dense ? 'single' : 'round',
|
|
991
|
-
borderColor:
|
|
1535
|
+
borderColor: panelBorderColor,
|
|
992
1536
|
paddingX: dense ? 0 : 1,
|
|
993
1537
|
width,
|
|
994
1538
|
marginBottom: marginBottom || 0
|
|
995
1539
|
},
|
|
996
|
-
React.createElement(
|
|
1540
|
+
React.createElement(
|
|
1541
|
+
Text,
|
|
1542
|
+
{ bold: true, color: titleColor, backgroundColor: titleBackground },
|
|
1543
|
+
truncate(title, contentWidth)
|
|
1544
|
+
),
|
|
997
1545
|
...visibleLines.map((line, index) => React.createElement(
|
|
998
1546
|
Text,
|
|
999
1547
|
{
|
|
@@ -1024,8 +1572,8 @@ function createDashboardApp(deps) {
|
|
|
1024
1572
|
{
|
|
1025
1573
|
key: tab.id,
|
|
1026
1574
|
bold: isActive,
|
|
1027
|
-
color: isActive ? '
|
|
1028
|
-
backgroundColor: isActive ? '
|
|
1575
|
+
color: isActive ? 'white' : 'gray',
|
|
1576
|
+
backgroundColor: isActive ? 'blue' : undefined
|
|
1029
1577
|
},
|
|
1030
1578
|
tab.label
|
|
1031
1579
|
);
|
|
@@ -1072,14 +1620,29 @@ function createDashboardApp(deps) {
|
|
|
1072
1620
|
const initialNormalizedError = initialError ? toDashboardError(initialError) : null;
|
|
1073
1621
|
const snapshotHashRef = useRef(safeJsonHash(initialSnapshot || null));
|
|
1074
1622
|
const errorHashRef = useRef(initialNormalizedError ? safeJsonHash(initialNormalizedError) : null);
|
|
1623
|
+
const lastVPressRef = useRef(0);
|
|
1075
1624
|
|
|
1076
1625
|
const [activeFlow, setActiveFlow] = useState(fallbackFlow);
|
|
1077
1626
|
const [snapshot, setSnapshot] = useState(initialSnapshot || null);
|
|
1078
1627
|
const [error, setError] = useState(initialNormalizedError);
|
|
1079
1628
|
const [ui, setUi] = useState(createInitialUIState());
|
|
1080
|
-
const [
|
|
1629
|
+
const [sectionFocus, setSectionFocus] = useState({
|
|
1630
|
+
runs: 'run-files',
|
|
1631
|
+
overview: 'project',
|
|
1632
|
+
health: 'stats'
|
|
1633
|
+
});
|
|
1634
|
+
const [selectionBySection, setSelectionBySection] = useState({
|
|
1635
|
+
'run-files': 0,
|
|
1636
|
+
pending: 0,
|
|
1637
|
+
completed: 0
|
|
1638
|
+
});
|
|
1639
|
+
const [expandedGroups, setExpandedGroups] = useState({});
|
|
1640
|
+
const [previewTarget, setPreviewTarget] = useState(null);
|
|
1081
1641
|
const [previewOpen, setPreviewOpen] = useState(false);
|
|
1642
|
+
const [paneFocus, setPaneFocus] = useState('main');
|
|
1643
|
+
const [overlayPreviewOpen, setOverlayPreviewOpen] = useState(false);
|
|
1082
1644
|
const [previewScroll, setPreviewScroll] = useState(0);
|
|
1645
|
+
const [statusLine, setStatusLine] = useState('');
|
|
1083
1646
|
const [lastRefreshAt, setLastRefreshAt] = useState(new Date().toISOString());
|
|
1084
1647
|
const [watchStatus, setWatchStatus] = useState(watchEnabled ? 'watching' : 'off');
|
|
1085
1648
|
const [terminalSize, setTerminalSize] = useState(() => ({
|
|
@@ -1102,9 +1665,42 @@ function createDashboardApp(deps) {
|
|
|
1102
1665
|
}
|
|
1103
1666
|
};
|
|
1104
1667
|
}, [parseSnapshotForFlow, parseSnapshot]);
|
|
1668
|
+
|
|
1669
|
+
const previewVisibleRows = Number.isFinite(terminalSize.rows) ? terminalSize.rows : (process.stdout.rows || 40);
|
|
1670
|
+
const showErrorPanelForSections = Boolean(error) && previewVisibleRows >= 18;
|
|
1671
|
+
const getAvailableSections = useCallback((viewId) => {
|
|
1672
|
+
const base = getSectionOrderForView(viewId);
|
|
1673
|
+
return base.filter((sectionKey) => sectionKey !== 'error-details' || showErrorPanelForSections);
|
|
1674
|
+
}, [showErrorPanelForSections]);
|
|
1675
|
+
|
|
1105
1676
|
const runFileEntries = getRunFileEntries(snapshot, activeFlow);
|
|
1106
|
-
const
|
|
1107
|
-
const
|
|
1677
|
+
const runFileRows = toRunFileRows(runFileEntries, activeFlow);
|
|
1678
|
+
const pendingRows = toExpandableRows(
|
|
1679
|
+
buildPendingGroups(snapshot, activeFlow),
|
|
1680
|
+
getNoPendingMessage(getEffectiveFlow(activeFlow, snapshot)),
|
|
1681
|
+
expandedGroups
|
|
1682
|
+
);
|
|
1683
|
+
const completedRows = toExpandableRows(
|
|
1684
|
+
buildCompletedGroups(snapshot, activeFlow),
|
|
1685
|
+
getNoCompletedMessage(getEffectiveFlow(activeFlow, snapshot)),
|
|
1686
|
+
expandedGroups
|
|
1687
|
+
);
|
|
1688
|
+
|
|
1689
|
+
const rowsBySection = {
|
|
1690
|
+
'run-files': runFileRows,
|
|
1691
|
+
pending: pendingRows,
|
|
1692
|
+
completed: completedRows
|
|
1693
|
+
};
|
|
1694
|
+
|
|
1695
|
+
const currentSectionOrder = getAvailableSections(ui.view);
|
|
1696
|
+
const focusedSection = currentSectionOrder.includes(sectionFocus[ui.view])
|
|
1697
|
+
? sectionFocus[ui.view]
|
|
1698
|
+
: (currentSectionOrder[0] || 'current-run');
|
|
1699
|
+
|
|
1700
|
+
const focusedRows = rowsBySection[focusedSection] || [];
|
|
1701
|
+
const focusedIndex = selectionBySection[focusedSection] || 0;
|
|
1702
|
+
const selectedFocusedRow = getSelectedRow(focusedRows, focusedIndex);
|
|
1703
|
+
const selectedFocusedFile = rowToFileEntry(selectedFocusedRow);
|
|
1108
1704
|
|
|
1109
1705
|
const refresh = useCallback(async () => {
|
|
1110
1706
|
const now = new Date().toISOString();
|
|
@@ -1166,41 +1762,6 @@ function createDashboardApp(deps) {
|
|
|
1166
1762
|
return;
|
|
1167
1763
|
}
|
|
1168
1764
|
|
|
1169
|
-
if (input === 'v' && ui.view === 'runs') {
|
|
1170
|
-
if (selectedFile) {
|
|
1171
|
-
setPreviewOpen((previous) => !previous);
|
|
1172
|
-
setPreviewScroll(0);
|
|
1173
|
-
}
|
|
1174
|
-
return;
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
|
-
if (key.escape && previewOpen) {
|
|
1178
|
-
setPreviewOpen(false);
|
|
1179
|
-
setPreviewScroll(0);
|
|
1180
|
-
return;
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
if (ui.view === 'runs' && (key.upArrow || key.downArrow || input === 'j' || input === 'k')) {
|
|
1184
|
-
const moveDown = key.downArrow || input === 'j';
|
|
1185
|
-
const moveUp = key.upArrow || input === 'k';
|
|
1186
|
-
|
|
1187
|
-
if (previewOpen) {
|
|
1188
|
-
if (moveDown) {
|
|
1189
|
-
setPreviewScroll((previous) => previous + 1);
|
|
1190
|
-
} else if (moveUp) {
|
|
1191
|
-
setPreviewScroll((previous) => Math.max(0, previous - 1));
|
|
1192
|
-
}
|
|
1193
|
-
return;
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
if (moveDown) {
|
|
1197
|
-
setSelectedFileIndex((previous) => clampIndex(previous + 1, runFileEntries.length));
|
|
1198
|
-
} else if (moveUp) {
|
|
1199
|
-
setSelectedFileIndex((previous) => clampIndex(previous - 1, runFileEntries.length));
|
|
1200
|
-
}
|
|
1201
|
-
return;
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
1765
|
if (input === 'h' || input === '?') {
|
|
1205
1766
|
setUi((previous) => ({ ...previous, showHelp: !previous.showHelp }));
|
|
1206
1767
|
return;
|
|
@@ -1208,31 +1769,19 @@ function createDashboardApp(deps) {
|
|
|
1208
1769
|
|
|
1209
1770
|
if (input === '1') {
|
|
1210
1771
|
setUi((previous) => ({ ...previous, view: 'runs' }));
|
|
1772
|
+
setPaneFocus('main');
|
|
1211
1773
|
return;
|
|
1212
1774
|
}
|
|
1213
1775
|
|
|
1214
1776
|
if (input === '2') {
|
|
1215
1777
|
setUi((previous) => ({ ...previous, view: 'overview' }));
|
|
1778
|
+
setPaneFocus('main');
|
|
1216
1779
|
return;
|
|
1217
1780
|
}
|
|
1218
1781
|
|
|
1219
1782
|
if (input === '3') {
|
|
1220
1783
|
setUi((previous) => ({ ...previous, view: 'health' }));
|
|
1221
|
-
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
if (key.tab) {
|
|
1225
|
-
setUi((previous) => ({ ...previous, view: cycleView(previous.view) }));
|
|
1226
|
-
return;
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
if (key.rightArrow) {
|
|
1230
|
-
setUi((previous) => ({ ...previous, view: cycleView(previous.view) }));
|
|
1231
|
-
return;
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
|
-
if (key.leftArrow) {
|
|
1235
|
-
setUi((previous) => ({ ...previous, view: cycleViewBackward(previous.view) }));
|
|
1784
|
+
setPaneFocus('main');
|
|
1236
1785
|
return;
|
|
1237
1786
|
}
|
|
1238
1787
|
|
|
@@ -1248,8 +1797,22 @@ function createDashboardApp(deps) {
|
|
|
1248
1797
|
: 0;
|
|
1249
1798
|
return availableFlowIds[nextIndex];
|
|
1250
1799
|
});
|
|
1800
|
+
setSelectionBySection({
|
|
1801
|
+
'run-files': 0,
|
|
1802
|
+
pending: 0,
|
|
1803
|
+
completed: 0
|
|
1804
|
+
});
|
|
1805
|
+
setSectionFocus({
|
|
1806
|
+
runs: 'run-files',
|
|
1807
|
+
overview: 'project',
|
|
1808
|
+
health: 'stats'
|
|
1809
|
+
});
|
|
1810
|
+
setExpandedGroups({});
|
|
1811
|
+
setPreviewTarget(null);
|
|
1251
1812
|
setPreviewOpen(false);
|
|
1813
|
+
setOverlayPreviewOpen(false);
|
|
1252
1814
|
setPreviewScroll(0);
|
|
1815
|
+
setPaneFocus('main');
|
|
1253
1816
|
return;
|
|
1254
1817
|
}
|
|
1255
1818
|
|
|
@@ -1265,8 +1828,210 @@ function createDashboardApp(deps) {
|
|
|
1265
1828
|
: 0;
|
|
1266
1829
|
return availableFlowIds[nextIndex];
|
|
1267
1830
|
});
|
|
1831
|
+
setSelectionBySection({
|
|
1832
|
+
'run-files': 0,
|
|
1833
|
+
pending: 0,
|
|
1834
|
+
completed: 0
|
|
1835
|
+
});
|
|
1836
|
+
setSectionFocus({
|
|
1837
|
+
runs: 'run-files',
|
|
1838
|
+
overview: 'project',
|
|
1839
|
+
health: 'stats'
|
|
1840
|
+
});
|
|
1841
|
+
setExpandedGroups({});
|
|
1842
|
+
setPreviewTarget(null);
|
|
1268
1843
|
setPreviewOpen(false);
|
|
1844
|
+
setOverlayPreviewOpen(false);
|
|
1269
1845
|
setPreviewScroll(0);
|
|
1846
|
+
setPaneFocus('main');
|
|
1847
|
+
return;
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
const availableSections = getAvailableSections(ui.view);
|
|
1851
|
+
const activeSection = availableSections.includes(sectionFocus[ui.view])
|
|
1852
|
+
? sectionFocus[ui.view]
|
|
1853
|
+
: (availableSections[0] || 'current-run');
|
|
1854
|
+
|
|
1855
|
+
if (key.tab && ui.view === 'runs' && previewOpen) {
|
|
1856
|
+
setPaneFocus((previous) => (previous === 'main' ? 'preview' : 'main'));
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
if (key.rightArrow || input === 'g') {
|
|
1861
|
+
setSectionFocus((previous) => ({
|
|
1862
|
+
...previous,
|
|
1863
|
+
[ui.view]: cycleSection(ui.view, activeSection, 1, availableSections)
|
|
1864
|
+
}));
|
|
1865
|
+
setPaneFocus('main');
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
if (key.leftArrow || input === 'G') {
|
|
1870
|
+
setSectionFocus((previous) => ({
|
|
1871
|
+
...previous,
|
|
1872
|
+
[ui.view]: cycleSection(ui.view, activeSection, -1, availableSections)
|
|
1873
|
+
}));
|
|
1874
|
+
setPaneFocus('main');
|
|
1875
|
+
return;
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
if (ui.view === 'runs') {
|
|
1879
|
+
if (input === 'a') {
|
|
1880
|
+
setSectionFocus((previous) => ({ ...previous, runs: 'current-run' }));
|
|
1881
|
+
setPaneFocus('main');
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
if (input === 'f') {
|
|
1885
|
+
setSectionFocus((previous) => ({ ...previous, runs: 'run-files' }));
|
|
1886
|
+
setPaneFocus('main');
|
|
1887
|
+
return;
|
|
1888
|
+
}
|
|
1889
|
+
if (input === 'p') {
|
|
1890
|
+
setSectionFocus((previous) => ({ ...previous, runs: 'pending' }));
|
|
1891
|
+
setPaneFocus('main');
|
|
1892
|
+
return;
|
|
1893
|
+
}
|
|
1894
|
+
if (input === 'c') {
|
|
1895
|
+
setSectionFocus((previous) => ({ ...previous, runs: 'completed' }));
|
|
1896
|
+
setPaneFocus('main');
|
|
1897
|
+
return;
|
|
1898
|
+
}
|
|
1899
|
+
} else if (ui.view === 'overview') {
|
|
1900
|
+
if (input === 'p') {
|
|
1901
|
+
setSectionFocus((previous) => ({ ...previous, overview: 'project' }));
|
|
1902
|
+
return;
|
|
1903
|
+
}
|
|
1904
|
+
if (input === 'i') {
|
|
1905
|
+
setSectionFocus((previous) => ({ ...previous, overview: 'intent-status' }));
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
1908
|
+
if (input === 's') {
|
|
1909
|
+
setSectionFocus((previous) => ({ ...previous, overview: 'standards' }));
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1912
|
+
} else if (ui.view === 'health') {
|
|
1913
|
+
if (input === 't') {
|
|
1914
|
+
setSectionFocus((previous) => ({ ...previous, health: 'stats' }));
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1917
|
+
if (input === 'w') {
|
|
1918
|
+
setSectionFocus((previous) => ({ ...previous, health: 'warnings' }));
|
|
1919
|
+
return;
|
|
1920
|
+
}
|
|
1921
|
+
if (input === 'e' && showErrorPanelForSections) {
|
|
1922
|
+
setSectionFocus((previous) => ({ ...previous, health: 'error-details' }));
|
|
1923
|
+
return;
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
if (key.escape) {
|
|
1928
|
+
if (overlayPreviewOpen) {
|
|
1929
|
+
setOverlayPreviewOpen(false);
|
|
1930
|
+
setPaneFocus('preview');
|
|
1931
|
+
return;
|
|
1932
|
+
}
|
|
1933
|
+
if (previewOpen) {
|
|
1934
|
+
setPreviewOpen(false);
|
|
1935
|
+
setPreviewScroll(0);
|
|
1936
|
+
setPaneFocus('main');
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
if (ui.view === 'runs' && (key.upArrow || key.downArrow || input === 'j' || input === 'k')) {
|
|
1942
|
+
const moveDown = key.downArrow || input === 'j';
|
|
1943
|
+
const moveUp = key.upArrow || input === 'k';
|
|
1944
|
+
|
|
1945
|
+
if (overlayPreviewOpen || (previewOpen && paneFocus === 'preview')) {
|
|
1946
|
+
if (moveDown) {
|
|
1947
|
+
setPreviewScroll((previous) => previous + 1);
|
|
1948
|
+
} else if (moveUp) {
|
|
1949
|
+
setPreviewScroll((previous) => Math.max(0, previous - 1));
|
|
1950
|
+
}
|
|
1951
|
+
return;
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
const targetSection = activeSection === 'current-run' ? 'run-files' : activeSection;
|
|
1955
|
+
if (targetSection !== activeSection) {
|
|
1956
|
+
setSectionFocus((previous) => ({ ...previous, runs: targetSection }));
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
const targetRows = rowsBySection[targetSection] || [];
|
|
1960
|
+
if (targetRows.length === 0) {
|
|
1961
|
+
return;
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
const currentIndex = selectionBySection[targetSection] || 0;
|
|
1965
|
+
const nextIndex = moveDown
|
|
1966
|
+
? moveRowSelection(targetRows, currentIndex, 1)
|
|
1967
|
+
: moveRowSelection(targetRows, currentIndex, -1);
|
|
1968
|
+
|
|
1969
|
+
setSelectionBySection((previous) => ({
|
|
1970
|
+
...previous,
|
|
1971
|
+
[targetSection]: nextIndex
|
|
1972
|
+
}));
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
if (ui.view === 'runs' && (key.return || key.enter)) {
|
|
1977
|
+
if (activeSection === 'pending' || activeSection === 'completed') {
|
|
1978
|
+
const rowsForSection = rowsBySection[activeSection] || [];
|
|
1979
|
+
const selectedRow = getSelectedRow(rowsForSection, selectionBySection[activeSection] || 0);
|
|
1980
|
+
if (selectedRow?.kind === 'group' && selectedRow.expandable) {
|
|
1981
|
+
setExpandedGroups((previous) => ({
|
|
1982
|
+
...previous,
|
|
1983
|
+
[selectedRow.key]: !previous[selectedRow.key]
|
|
1984
|
+
}));
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
return;
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
if (input === 'v' && ui.view === 'runs') {
|
|
1991
|
+
const target = selectedFocusedFile || previewTarget;
|
|
1992
|
+
if (!target) {
|
|
1993
|
+
setStatusLine('Select a file row first (run files, pending, or completed).');
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
const now = Date.now();
|
|
1998
|
+
const isDoublePress = (now - lastVPressRef.current) <= 320;
|
|
1999
|
+
lastVPressRef.current = now;
|
|
2000
|
+
|
|
2001
|
+
if (isDoublePress) {
|
|
2002
|
+
setPreviewTarget(target);
|
|
2003
|
+
setPreviewOpen(true);
|
|
2004
|
+
setOverlayPreviewOpen(true);
|
|
2005
|
+
setPreviewScroll(0);
|
|
2006
|
+
setPaneFocus('preview');
|
|
2007
|
+
return;
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
if (!previewOpen) {
|
|
2011
|
+
setPreviewTarget(target);
|
|
2012
|
+
setPreviewOpen(true);
|
|
2013
|
+
setOverlayPreviewOpen(false);
|
|
2014
|
+
setPreviewScroll(0);
|
|
2015
|
+
setPaneFocus('main');
|
|
2016
|
+
return;
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
if (overlayPreviewOpen) {
|
|
2020
|
+
setOverlayPreviewOpen(false);
|
|
2021
|
+
setPaneFocus('preview');
|
|
2022
|
+
return;
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
setPreviewOpen(false);
|
|
2026
|
+
setPreviewScroll(0);
|
|
2027
|
+
setPaneFocus('main');
|
|
2028
|
+
return;
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
if (input === 'o' && ui.view === 'runs') {
|
|
2032
|
+
const target = selectedFocusedFile || previewTarget;
|
|
2033
|
+
const result = openFileWithDefaultApp(target?.path);
|
|
2034
|
+
setStatusLine(result.message);
|
|
1270
2035
|
}
|
|
1271
2036
|
});
|
|
1272
2037
|
|
|
@@ -1275,20 +2040,51 @@ function createDashboardApp(deps) {
|
|
|
1275
2040
|
}, [refresh]);
|
|
1276
2041
|
|
|
1277
2042
|
useEffect(() => {
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
2043
|
+
setSelectionBySection((previous) => ({
|
|
2044
|
+
...previous,
|
|
2045
|
+
'run-files': clampIndex(previous['run-files'] || 0, runFileRows.length),
|
|
2046
|
+
pending: clampIndex(previous.pending || 0, pendingRows.length),
|
|
2047
|
+
completed: clampIndex(previous.completed || 0, completedRows.length)
|
|
2048
|
+
}));
|
|
2049
|
+
}, [activeFlow, runFileRows.length, pendingRows.length, completedRows.length, snapshot?.generatedAt]);
|
|
1284
2050
|
|
|
1285
2051
|
useEffect(() => {
|
|
1286
2052
|
if (ui.view !== 'runs') {
|
|
1287
2053
|
setPreviewOpen(false);
|
|
2054
|
+
setOverlayPreviewOpen(false);
|
|
1288
2055
|
setPreviewScroll(0);
|
|
2056
|
+
setPaneFocus('main');
|
|
1289
2057
|
}
|
|
1290
2058
|
}, [ui.view]);
|
|
1291
2059
|
|
|
2060
|
+
useEffect(() => {
|
|
2061
|
+
if (!previewOpen || overlayPreviewOpen || paneFocus !== 'main') {
|
|
2062
|
+
return;
|
|
2063
|
+
}
|
|
2064
|
+
if (!selectedFocusedFile?.path) {
|
|
2065
|
+
return;
|
|
2066
|
+
}
|
|
2067
|
+
if (previewTarget?.path === selectedFocusedFile.path) {
|
|
2068
|
+
return;
|
|
2069
|
+
}
|
|
2070
|
+
setPreviewTarget(selectedFocusedFile);
|
|
2071
|
+
setPreviewScroll(0);
|
|
2072
|
+
}, [previewOpen, overlayPreviewOpen, paneFocus, selectedFocusedFile?.path, previewTarget?.path]);
|
|
2073
|
+
|
|
2074
|
+
useEffect(() => {
|
|
2075
|
+
if (statusLine === '') {
|
|
2076
|
+
return undefined;
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
const timeout = setTimeout(() => {
|
|
2080
|
+
setStatusLine('');
|
|
2081
|
+
}, 3500);
|
|
2082
|
+
|
|
2083
|
+
return () => {
|
|
2084
|
+
clearTimeout(timeout);
|
|
2085
|
+
};
|
|
2086
|
+
}, [statusLine]);
|
|
2087
|
+
|
|
1292
2088
|
useEffect(() => {
|
|
1293
2089
|
if (!stdout || typeof stdout.on !== 'function') {
|
|
1294
2090
|
setTerminalSize({
|
|
@@ -1358,12 +2154,20 @@ function createDashboardApp(deps) {
|
|
|
1358
2154
|
};
|
|
1359
2155
|
}, [watchEnabled, refreshMs, refresh, rootPath, workspacePath, resolveRootPathForFlow, activeFlow]);
|
|
1360
2156
|
|
|
2157
|
+
useEffect(() => {
|
|
2158
|
+
if (!stdout || typeof stdout.write !== 'function') {
|
|
2159
|
+
return;
|
|
2160
|
+
}
|
|
2161
|
+
if (stdout.isTTY === false) {
|
|
2162
|
+
return;
|
|
2163
|
+
}
|
|
2164
|
+
stdout.write('\u001B[2J\u001B[3J\u001B[H');
|
|
2165
|
+
}, [stdout]);
|
|
2166
|
+
|
|
1361
2167
|
const cols = Number.isFinite(terminalSize.columns) ? terminalSize.columns : (process.stdout.columns || 120);
|
|
1362
2168
|
const rows = Number.isFinite(terminalSize.rows) ? terminalSize.rows : (process.stdout.rows || 40);
|
|
1363
2169
|
|
|
1364
2170
|
const fullWidth = Math.max(40, cols - 1);
|
|
1365
|
-
const compactWidth = Math.max(18, fullWidth - 4);
|
|
1366
|
-
|
|
1367
2171
|
const showHelpLine = ui.showHelp && rows >= 14;
|
|
1368
2172
|
const showErrorPanel = Boolean(error) && rows >= 18;
|
|
1369
2173
|
const showErrorInline = Boolean(error) && !showErrorPanel;
|
|
@@ -1373,28 +2177,72 @@ function createDashboardApp(deps) {
|
|
|
1373
2177
|
const contentRowsBudget = Math.max(4, rows - reservedRows);
|
|
1374
2178
|
const ultraCompact = rows <= 14;
|
|
1375
2179
|
const panelTitles = getPanelTitles(activeFlow, snapshot);
|
|
1376
|
-
const
|
|
1377
|
-
const
|
|
2180
|
+
const splitPreviewLayout = ui.view === 'runs' && previewOpen && !overlayPreviewOpen && cols >= 110 && rows >= 16;
|
|
2181
|
+
const mainPaneWidth = splitPreviewLayout
|
|
2182
|
+
? Math.max(34, Math.floor((fullWidth - 1) * 0.52))
|
|
2183
|
+
: fullWidth;
|
|
2184
|
+
const previewPaneWidth = splitPreviewLayout
|
|
2185
|
+
? Math.max(30, fullWidth - mainPaneWidth - 1)
|
|
2186
|
+
: fullWidth;
|
|
2187
|
+
const mainCompactWidth = Math.max(18, mainPaneWidth - 4);
|
|
2188
|
+
const previewCompactWidth = Math.max(18, previewPaneWidth - 4);
|
|
2189
|
+
|
|
2190
|
+
const runFileLines = buildInteractiveRowsLines(
|
|
2191
|
+
runFileRows,
|
|
2192
|
+
selectionBySection['run-files'] || 0,
|
|
2193
|
+
icons,
|
|
2194
|
+
mainCompactWidth,
|
|
2195
|
+
ui.view === 'runs' && focusedSection === 'run-files' && paneFocus === 'main'
|
|
2196
|
+
);
|
|
2197
|
+
const pendingLines = buildInteractiveRowsLines(
|
|
2198
|
+
pendingRows,
|
|
2199
|
+
selectionBySection.pending || 0,
|
|
2200
|
+
icons,
|
|
2201
|
+
mainCompactWidth,
|
|
2202
|
+
ui.view === 'runs' && focusedSection === 'pending' && paneFocus === 'main'
|
|
2203
|
+
);
|
|
2204
|
+
const completedLines = buildInteractiveRowsLines(
|
|
2205
|
+
completedRows,
|
|
2206
|
+
selectionBySection.completed || 0,
|
|
2207
|
+
icons,
|
|
2208
|
+
mainCompactWidth,
|
|
2209
|
+
ui.view === 'runs' && focusedSection === 'completed' && paneFocus === 'main'
|
|
2210
|
+
);
|
|
2211
|
+
const effectivePreviewTarget = previewTarget || selectedFocusedFile;
|
|
2212
|
+
const previewLines = previewOpen
|
|
2213
|
+
? buildPreviewLines(effectivePreviewTarget, previewCompactWidth, previewScroll, {
|
|
2214
|
+
fullDocument: overlayPreviewOpen
|
|
2215
|
+
})
|
|
2216
|
+
: [];
|
|
1378
2217
|
|
|
1379
2218
|
let panelCandidates;
|
|
1380
|
-
if (ui.view === '
|
|
2219
|
+
if (ui.view === 'runs' && previewOpen && overlayPreviewOpen) {
|
|
2220
|
+
panelCandidates = [
|
|
2221
|
+
{
|
|
2222
|
+
key: 'preview-overlay',
|
|
2223
|
+
title: `Preview: ${effectivePreviewTarget?.label || 'unknown'}`,
|
|
2224
|
+
lines: previewLines,
|
|
2225
|
+
borderColor: 'magenta'
|
|
2226
|
+
}
|
|
2227
|
+
];
|
|
2228
|
+
} else if (ui.view === 'overview') {
|
|
1381
2229
|
panelCandidates = [
|
|
1382
2230
|
{
|
|
1383
2231
|
key: 'project',
|
|
1384
2232
|
title: 'Project + Workspace',
|
|
1385
|
-
lines: buildOverviewProjectLines(snapshot,
|
|
2233
|
+
lines: buildOverviewProjectLines(snapshot, mainCompactWidth, activeFlow),
|
|
1386
2234
|
borderColor: 'green'
|
|
1387
2235
|
},
|
|
1388
2236
|
{
|
|
1389
2237
|
key: 'intent-status',
|
|
1390
2238
|
title: 'Intent Status',
|
|
1391
|
-
lines: buildOverviewIntentLines(snapshot,
|
|
2239
|
+
lines: buildOverviewIntentLines(snapshot, mainCompactWidth, activeFlow),
|
|
1392
2240
|
borderColor: 'yellow'
|
|
1393
2241
|
},
|
|
1394
2242
|
{
|
|
1395
2243
|
key: 'standards',
|
|
1396
2244
|
title: 'Standards',
|
|
1397
|
-
lines: buildOverviewStandardsLines(snapshot,
|
|
2245
|
+
lines: buildOverviewStandardsLines(snapshot, mainCompactWidth, activeFlow),
|
|
1398
2246
|
borderColor: 'blue'
|
|
1399
2247
|
}
|
|
1400
2248
|
];
|
|
@@ -1403,13 +2251,13 @@ function createDashboardApp(deps) {
|
|
|
1403
2251
|
{
|
|
1404
2252
|
key: 'stats',
|
|
1405
2253
|
title: 'Stats',
|
|
1406
|
-
lines: buildStatsLines(snapshot,
|
|
2254
|
+
lines: buildStatsLines(snapshot, mainCompactWidth, activeFlow),
|
|
1407
2255
|
borderColor: 'magenta'
|
|
1408
2256
|
},
|
|
1409
2257
|
{
|
|
1410
2258
|
key: 'warnings',
|
|
1411
2259
|
title: 'Warnings',
|
|
1412
|
-
lines: buildWarningsLines(snapshot,
|
|
2260
|
+
lines: buildWarningsLines(snapshot, mainCompactWidth),
|
|
1413
2261
|
borderColor: 'red'
|
|
1414
2262
|
}
|
|
1415
2263
|
];
|
|
@@ -1418,16 +2266,17 @@ function createDashboardApp(deps) {
|
|
|
1418
2266
|
panelCandidates.push({
|
|
1419
2267
|
key: 'error-details',
|
|
1420
2268
|
title: 'Error Details',
|
|
1421
|
-
lines: buildErrorLines(error,
|
|
2269
|
+
lines: buildErrorLines(error, mainCompactWidth),
|
|
1422
2270
|
borderColor: 'red'
|
|
1423
2271
|
});
|
|
1424
2272
|
}
|
|
1425
2273
|
} else {
|
|
2274
|
+
const includeInlinePreviewPanel = previewOpen && !splitPreviewLayout;
|
|
1426
2275
|
panelCandidates = [
|
|
1427
2276
|
{
|
|
1428
2277
|
key: 'current-run',
|
|
1429
2278
|
title: panelTitles.current,
|
|
1430
|
-
lines: buildCurrentRunLines(snapshot,
|
|
2279
|
+
lines: buildCurrentRunLines(snapshot, mainCompactWidth, activeFlow),
|
|
1431
2280
|
borderColor: 'green'
|
|
1432
2281
|
},
|
|
1433
2282
|
{
|
|
@@ -1436,10 +2285,10 @@ function createDashboardApp(deps) {
|
|
|
1436
2285
|
lines: runFileLines,
|
|
1437
2286
|
borderColor: 'yellow'
|
|
1438
2287
|
},
|
|
1439
|
-
|
|
2288
|
+
includeInlinePreviewPanel
|
|
1440
2289
|
? {
|
|
1441
2290
|
key: 'preview',
|
|
1442
|
-
title: `Preview: ${
|
|
2291
|
+
title: `Preview: ${effectivePreviewTarget?.label || 'unknown'}`,
|
|
1443
2292
|
lines: previewLines,
|
|
1444
2293
|
borderColor: 'magenta'
|
|
1445
2294
|
}
|
|
@@ -1447,19 +2296,19 @@ function createDashboardApp(deps) {
|
|
|
1447
2296
|
{
|
|
1448
2297
|
key: 'pending',
|
|
1449
2298
|
title: panelTitles.pending,
|
|
1450
|
-
lines:
|
|
2299
|
+
lines: pendingLines,
|
|
1451
2300
|
borderColor: 'yellow'
|
|
1452
2301
|
},
|
|
1453
2302
|
{
|
|
1454
2303
|
key: 'completed',
|
|
1455
2304
|
title: panelTitles.completed,
|
|
1456
|
-
lines:
|
|
2305
|
+
lines: completedLines,
|
|
1457
2306
|
borderColor: 'blue'
|
|
1458
2307
|
}
|
|
1459
2308
|
];
|
|
1460
2309
|
}
|
|
1461
2310
|
|
|
1462
|
-
if (ultraCompact) {
|
|
2311
|
+
if (ultraCompact && !splitPreviewLayout) {
|
|
1463
2312
|
if (previewOpen) {
|
|
1464
2313
|
panelCandidates = panelCandidates.filter((panel) => panel && (panel.key === 'current-run' || panel.key === 'preview'));
|
|
1465
2314
|
} else {
|
|
@@ -1469,8 +2318,83 @@ function createDashboardApp(deps) {
|
|
|
1469
2318
|
|
|
1470
2319
|
const panels = allocateSingleColumnPanels(panelCandidates, contentRowsBudget);
|
|
1471
2320
|
const flowSwitchHint = availableFlowIds.length > 1 ? ' | [ or ] switch flow' : '';
|
|
1472
|
-
const
|
|
1473
|
-
|
|
2321
|
+
const sectionHint = ui.view === 'runs'
|
|
2322
|
+
? ' | a active | f files | p pending | c done'
|
|
2323
|
+
: (ui.view === 'overview' ? ' | p project | i intents | s standards' : ' | t stats | w warnings | e errors');
|
|
2324
|
+
const previewHint = ui.view === 'runs'
|
|
2325
|
+
? (previewOpen
|
|
2326
|
+
? ` | tab ${paneFocus === 'preview' ? 'main' : 'preview'} | ↑/↓ ${paneFocus === 'preview' ? 'scroll' : 'navigate'} | v close | vv fullscreen`
|
|
2327
|
+
: ' | ↑/↓ navigate | enter expand | v preview | vv fullscreen | o open')
|
|
2328
|
+
: '';
|
|
2329
|
+
const helpText = `q quit | r refresh | h/? help | 1 runs | 2 overview | 3 health | g/G section${sectionHint}${previewHint}${flowSwitchHint}`;
|
|
2330
|
+
|
|
2331
|
+
const renderPanel = (panel, index, width, isFocused) => React.createElement(SectionPanel, {
|
|
2332
|
+
key: panel.key,
|
|
2333
|
+
title: panel.title,
|
|
2334
|
+
lines: panel.lines,
|
|
2335
|
+
width,
|
|
2336
|
+
maxLines: panel.maxLines,
|
|
2337
|
+
borderColor: panel.borderColor,
|
|
2338
|
+
marginBottom: densePanels ? 0 : (index === panels.length - 1 ? 0 : 1),
|
|
2339
|
+
dense: densePanels,
|
|
2340
|
+
focused: isFocused
|
|
2341
|
+
});
|
|
2342
|
+
|
|
2343
|
+
let contentNode;
|
|
2344
|
+
if (splitPreviewLayout && ui.view === 'runs' && !overlayPreviewOpen) {
|
|
2345
|
+
const previewPanel = {
|
|
2346
|
+
key: 'preview-split',
|
|
2347
|
+
title: `Preview: ${effectivePreviewTarget?.label || 'unknown'}`,
|
|
2348
|
+
lines: previewLines,
|
|
2349
|
+
borderColor: 'magenta',
|
|
2350
|
+
maxLines: Math.max(4, contentRowsBudget)
|
|
2351
|
+
};
|
|
2352
|
+
|
|
2353
|
+
contentNode = React.createElement(
|
|
2354
|
+
Box,
|
|
2355
|
+
{ width: fullWidth, flexDirection: 'row' },
|
|
2356
|
+
React.createElement(
|
|
2357
|
+
Box,
|
|
2358
|
+
{ width: mainPaneWidth, flexDirection: 'column' },
|
|
2359
|
+
...panels.map((panel, index) => React.createElement(SectionPanel, {
|
|
2360
|
+
key: panel.key,
|
|
2361
|
+
title: panel.title,
|
|
2362
|
+
lines: panel.lines,
|
|
2363
|
+
width: mainPaneWidth,
|
|
2364
|
+
maxLines: panel.maxLines,
|
|
2365
|
+
borderColor: panel.borderColor,
|
|
2366
|
+
marginBottom: densePanels ? 0 : (index === panels.length - 1 ? 0 : 1),
|
|
2367
|
+
dense: densePanels,
|
|
2368
|
+
focused: paneFocus === 'main' && panel.key === focusedSection
|
|
2369
|
+
}))
|
|
2370
|
+
),
|
|
2371
|
+
React.createElement(Box, { width: 1 }, React.createElement(Text, null, ' ')),
|
|
2372
|
+
React.createElement(
|
|
2373
|
+
Box,
|
|
2374
|
+
{ width: previewPaneWidth, flexDirection: 'column' },
|
|
2375
|
+
React.createElement(SectionPanel, {
|
|
2376
|
+
key: previewPanel.key,
|
|
2377
|
+
title: previewPanel.title,
|
|
2378
|
+
lines: previewPanel.lines,
|
|
2379
|
+
width: previewPaneWidth,
|
|
2380
|
+
maxLines: previewPanel.maxLines,
|
|
2381
|
+
borderColor: previewPanel.borderColor,
|
|
2382
|
+
marginBottom: 0,
|
|
2383
|
+
dense: densePanels,
|
|
2384
|
+
focused: paneFocus === 'preview'
|
|
2385
|
+
})
|
|
2386
|
+
)
|
|
2387
|
+
);
|
|
2388
|
+
} else {
|
|
2389
|
+
contentNode = panels.map((panel, index) => renderPanel(
|
|
2390
|
+
panel,
|
|
2391
|
+
index,
|
|
2392
|
+
fullWidth,
|
|
2393
|
+
(panel.key === 'preview' || panel.key === 'preview-overlay')
|
|
2394
|
+
? paneFocus === 'preview'
|
|
2395
|
+
: (paneFocus === 'main' && panel.key === focusedSection)
|
|
2396
|
+
));
|
|
2397
|
+
}
|
|
1474
2398
|
|
|
1475
2399
|
return React.createElement(
|
|
1476
2400
|
Box,
|
|
@@ -1484,24 +2408,19 @@ function createDashboardApp(deps) {
|
|
|
1484
2408
|
showErrorPanel
|
|
1485
2409
|
? React.createElement(SectionPanel, {
|
|
1486
2410
|
title: 'Errors',
|
|
1487
|
-
lines: buildErrorLines(error,
|
|
2411
|
+
lines: buildErrorLines(error, Math.max(18, fullWidth - 4)),
|
|
1488
2412
|
width: fullWidth,
|
|
1489
2413
|
maxLines: 2,
|
|
1490
2414
|
borderColor: 'red',
|
|
1491
2415
|
marginBottom: densePanels ? 0 : 1,
|
|
1492
|
-
dense: densePanels
|
|
2416
|
+
dense: densePanels,
|
|
2417
|
+
focused: paneFocus === 'main' && focusedSection === 'error-details'
|
|
1493
2418
|
})
|
|
1494
2419
|
: null,
|
|
1495
|
-
...
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
width: fullWidth,
|
|
1500
|
-
maxLines: panel.maxLines,
|
|
1501
|
-
borderColor: panel.borderColor,
|
|
1502
|
-
marginBottom: densePanels ? 0 : (index === panels.length - 1 ? 0 : 1),
|
|
1503
|
-
dense: densePanels
|
|
1504
|
-
})),
|
|
2420
|
+
...(Array.isArray(contentNode) ? contentNode : [contentNode]),
|
|
2421
|
+
statusLine !== ''
|
|
2422
|
+
? React.createElement(Text, { color: 'yellow' }, truncate(statusLine, fullWidth))
|
|
2423
|
+
: null,
|
|
1505
2424
|
showHelpLine
|
|
1506
2425
|
? React.createElement(Text, { color: 'gray' }, truncate(helpText, fullWidth))
|
|
1507
2426
|
: null
|